diff --git a/.changeset/six-steaks-talk.md b/.changeset/six-steaks-talk.md new file mode 100644 index 0000000..4bfe2cf --- /dev/null +++ b/.changeset/six-steaks-talk.md @@ -0,0 +1,5 @@ +--- +'@theguild/federation-composition': minor +--- + +Support @interfaceObject directive diff --git a/.changeset/slimy-ads-dress.md b/.changeset/slimy-ads-dress.md new file mode 100644 index 0000000..f996f72 --- /dev/null +++ b/.changeset/slimy-ads-dress.md @@ -0,0 +1,5 @@ +--- +'@theguild/federation-composition': patch +--- + +Improve INTERFACE_KEY_MISSING_IMPLEMENTATION_TYPE diff --git a/README.md b/README.md index d674703..42b3a50 100644 --- a/README.md +++ b/README.md @@ -179,31 +179,18 @@ Your feedback and bug reports are welcome and appreciated. - ✅ `REQUIRES_DIRECTIVE_IN_FIELDS_ARG` - ✅ `TYPE_DEFINITION_INVALID` - ✅ `OVERRIDE_COLLISION_WITH_ANOTHER_DIRECTIVE` +- ✅ `INTERFACE_OBJECT_USAGE_ERROR` +- ✅ `REQUIRED_INACCESSIBLE` +- ✅ `SATISFIABILITY_ERROR` ### TODOs -- [ ] `INTERFACE_OBJECT_USAGE_ERROR` - [ ] `INTERFACE_FIELD_NO_IMPLEM` -- [ ] `SATISFIABILITY_ERROR` - [ ] `DISALLOWED_INACCESSIBLE` -- [ ] `DOWNSTREAM_SERVICE_ERROR` - [ ] `EXTERNAL_ARGUMENT_DEFAULT_MISMATCH` - [ ] `EXTERNAL_ARGUMENT_TYPE_MISMATCH` - [ ] `EXTERNAL_COLLISION_WITH_ANOTHER_DIRECTIVE` - [ ] `IMPLEMENTED_BY_INACCESSIBLE` -- [ ] `INVALID_FEDERATION_SUPERGRAPH` - [ ] `LINK_IMPORT_NAME_MISMATCH` -- [ ] `REQUIRED_INACCESSIBLE` -- [ ] `SHAREABLE_HAS_MISMATCHED_RUNTIME_TYPES` -- [ ] `UNSUPPORTED_FEATURE` - [ ] `UNSUPPORTED_LINKED_FEATURE` - [ ] `TYPE_WITH_ONLY_UNUSED_EXTERNAL` -- [ ] `SATISFIABILITY_ERROR` - deeply nested key fields -- [ ] `SATISFIABILITY_ERROR` - fragments in keys -- [ ] `SATISFIABILITY_ERROR` - support interfaces... (kill me) -- [ ] `SATISFIABILITY_ERROR` - @require - check if fields defined by @require can be resolved by - current subgraph or by moving to other subgraphs. -- [ ] `SATISFIABILITY_ERROR` - @provides? -- [ ] more accurate key fields comparison (I did string ≠ string but we need to make it better) -- [ ] support `@interfaceObject` -- [ ] support `[String!]!` and `[String!]` comparison, not only `String!` vs `String` diff --git a/__tests__/composition.spec.ts b/__tests__/composition.spec.ts index 763be43..0b99a69 100644 --- a/__tests__/composition.spec.ts +++ b/__tests__/composition.spec.ts @@ -18,14 +18,8 @@ expect.addSnapshotSerializer({ }); testImplementations(api => { - const library = api.library; const composeServices = api.composeServices; - function logSupergraph(result: { supergraphSdl: string }) { - console.log('--', library, '--'); - console.log(result.supergraphSdl); - } - test('duplicated Query fields', () => { const result = composeServices([ { @@ -2960,9 +2954,6 @@ testImplementations(api => { if (satisfiesVersionRange('< v2.3', version)) { assertCompositionFailure(result); - if (api.library === 'apollo') { - console.log(JSON.stringify(result.errors)); - } expect(result.errors).toContainEqual( expect.objectContaining({ message: '[a] Cannot import unknown element "@interfaceObject".', diff --git a/__tests__/graphql/transform-supergraph-to-public-schema.spec.ts b/__tests__/graphql/transform-supergraph-to-public-schema.spec.ts index d72d086..c529193 100644 --- a/__tests__/graphql/transform-supergraph-to-public-schema.spec.ts +++ b/__tests__/graphql/transform-supergraph-to-public-schema.spec.ts @@ -275,7 +275,7 @@ describe('transformSupergraphToPublicSchema', () => { const resultSdl = transformSupergraphToPublicSchema(sdl); expect(print(resultSdl)).toMatchInlineSnapshot(` "type Query { - foo: String @deprecated(reason: \\"jooo\\") + foo: String @deprecated(reason: "jooo") }" `); }); diff --git a/__tests__/interface-object-composition.spec.ts b/__tests__/interface-object-composition.spec.ts index 98389d6..ad836b3 100644 --- a/__tests__/interface-object-composition.spec.ts +++ b/__tests__/interface-object-composition.spec.ts @@ -1,14 +1,14 @@ import { parse } from 'graphql'; import { describe, expect, test } from 'vitest'; -import { composeServices, CompositionFailure, CompositionSuccess } from '../src'; +import { assertCompositionFailure, assertCompositionSuccess } from '../src'; import { testImplementations } from './shared/testkit'; -testImplementations(_ => { +testImplementations(api => { describe('interface object composition', () => { test('if link directive is not present on all subgraphs, composition should fail', () => { - const result = composeServices([ + const result = api.composeServices([ { - name: 'subgraphA', + name: 'a', typeDefs: parse(/* GraphQL */ ` extend schema @link( @@ -27,7 +27,7 @@ testImplementations(_ => { `), }, { - name: 'subgraphB', + name: 'b', typeDefs: parse(/* GraphQL */ ` type Query { otherField: String @@ -39,18 +39,26 @@ testImplementations(_ => { } `), }, - ]) as CompositionFailure; - expect(result.errors).toMatchInlineSnapshot(` - [ - [GraphQLError: [subgraphB] Unknown directive "@interfaceObject".], - ] - `); + ]); + + assertCompositionFailure(result); + + expect(result.errors).toContainEqual( + expect.objectContaining({ + message: `[b] Unknown directive "@interfaceObject". If you meant the "@interfaceObject" federation 2 directive, note that this schema is a federation 1 schema. To be a federation 2 schema, it needs to @link to the federation ${ + api.library === 'apollo' ? 'specifcation' : 'specification' + } v2.`, + extensions: expect.objectContaining({ + code: 'INVALID_GRAPHQL', + }), + }), + ); }); test('link directive should have url pointing to federation > 2.3 to enable @interfaceObject on all subgraphs', () => { - const result = composeServices([ + const result = api.composeServices([ { - name: 'subgraphA', + name: 'a', typeDefs: parse(/* GraphQL */ ` extend schema @link( @@ -74,7 +82,7 @@ testImplementations(_ => { `), }, { - name: 'subgraphB', + name: 'b', typeDefs: parse(/* GraphQL */ ` extend schema @link( @@ -91,22 +99,24 @@ testImplementations(_ => { } `), }, - ]) as CompositionFailure; + ]); + assertCompositionFailure(result); expect(result.errors).toMatchInlineSnapshot(` [ - [GraphQLError: [subgraphA] Cannot import unknown element "@interfaceObject".], + [GraphQLError: [a] Cannot import unknown element "@interfaceObject".], ] `); + }); - // define success case - const result2 = composeServices([ + test('@external + @requires + @interfaceObject', () => { + const result = api.composeServices([ { - name: 'subgraphA', + name: 'a', typeDefs: parse(/* GraphQL */ ` extend schema @link( url: "https://specs.apollo.dev/federation/v2.3" - import: ["@key", "@interfaceObject"] + import: ["@key", "@interfaceObject", "@shareable"] ) type Query { @@ -120,21 +130,18 @@ testImplementations(_ => { type MyType implements MyInterface @key(fields: "id") { id: ID! - field: String + field: String @shareable } `), }, { - name: 'subgraphB', + name: 'b', typeDefs: parse(/* GraphQL */ ` extend schema @link( url: "https://specs.apollo.dev/federation/v2.3" import: ["@key", "@interfaceObject"] ) - type Query { - otherField: String - } type MyInterface @key(fields: "id") @interfaceObject { id: ID! @@ -142,14 +149,42 @@ testImplementations(_ => { } `), }, - ]) as CompositionSuccess; - expect(result2.supergraphSdl).toBeDefined(); + { + name: 'c', + typeDefs: parse(/* GraphQL */ ` + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key", "@interfaceObject", "@shareable", "@requires", "@external"] + ) + + type MyInterface @key(fields: "id", resolvable: false) @interfaceObject { + id: ID! + newField: String @external + field: String @shareable @requires(fields: "newField") + } + `), + }, + ]); + + assertCompositionSuccess(result); + + expect(result.supergraphSdl).toContainGraphQL(/* GraphQL */ ` + interface MyInterface + @join__type(graph: A, key: "id") + @join__type(graph: B, key: "id", isInterfaceObject: true) + @join__type(graph: C, key: "id", isInterfaceObject: true, resolvable: false) { + id: ID! + field: String @join__field(graph: A) @join__field(graph: C, requires: "newField") + newField: String @join__field(external: true, graph: C) @join__field(graph: B) + } + `); }); - test(`link directive should have @interfaceObject in 'import' array on all subgraphs`, () => { - const result = composeServices([ + test('link directive does not have to import @interfaceObject in all subgraphs', () => { + const result = api.composeServices([ { - name: 'subgraphA', + name: 'a', typeDefs: parse(/* GraphQL */ ` extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"]) @@ -169,7 +204,7 @@ testImplementations(_ => { `), }, { - name: 'subgraphB', + name: 'b', typeDefs: parse(/* GraphQL */ ` extend schema @link( @@ -186,18 +221,14 @@ testImplementations(_ => { } `), }, - ]) as CompositionFailure; - expect(result.errors).toMatchInlineSnapshot(` - [ - [GraphQLError: For @interfaceObject to work, there is must be an entity interface defined in the different subgraph. Interface MyInterface in subgraph SUBGRAPH_A is good candidate, but it doesn't satisfy the requirements on version (>= 2.3) or imports (@key, @interfaceObject). Maybe check those?], - ] - `); + ]); + assertCompositionSuccess(result); }); test(`target interface must have @key directive on subgraph where it's defined`, () => { - const result = composeServices([ + const result = api.composeServices([ { - name: 'subgraphA', + name: 'a', typeDefs: parse(/* GraphQL */ ` extend schema @link( @@ -221,7 +252,7 @@ testImplementations(_ => { `), }, { - name: 'subgraphB', + name: 'b', typeDefs: parse(/* GraphQL */ ` extend schema @link( @@ -238,12 +269,9 @@ testImplementations(_ => { } `), }, - ]) as CompositionFailure; - expect(result.errors).toMatchInlineSnapshot(` - [ - [GraphQLError: @key directive must be present on interface type MyInterface in subgraph SUBGRAPH_A for @objectInterface to work], - ] - `); + ]); + + assertCompositionSuccess(result); }); // Subgraph A must define every entity type in your entire supergraph that implements MyInterface. @@ -251,10 +279,10 @@ testImplementations(_ => { // You can think of a subgraph that defines an entity interface as also owning every entity that implements that interface. // this case is really unclear. // documentation: https://www.apollographql.com/docs/federation/federated-types/interfaces/#usage-rules - test.skip(`subgraph where interface is defined must have all entity types which implement that interface defined `, () => { - const result = composeServices([ + test(`subgraph where interface is defined must have all entity types which implement that interface defined `, () => { + const result = api.composeServices([ { - name: 'subgraphA', + name: 'a', typeDefs: parse(/* GraphQL */ ` extend schema @link( @@ -273,7 +301,7 @@ testImplementations(_ => { `), }, { - name: 'subgraphB', + name: 'b', typeDefs: parse(/* GraphQL */ ` extend schema @link( @@ -295,17 +323,26 @@ testImplementations(_ => { } `), }, - ]) as CompositionFailure; - expect(result.errors).toMatchInlineSnapshot(``); - // should fail + ]); + + assertCompositionFailure(result); + + expect(result.errors).toContainEqual( + expect.objectContaining({ + message: '[b] Cannot implement non-interface type MyInterface (of type ObjectType)', + extensions: expect.objectContaining({ + code: 'INVALID_GRAPHQL', + }), + }), + ); }); describe(`@interfaceObject definition`, () => { describe(`at least one other subgraph must define an interface type with @key directive which has the same name as the object type with @interfaceObject`, () => { test(`interface type is not present on any subgraph. Should fail`, () => { - const result = composeServices([ + const result = api.composeServices([ { - name: 'subgraphA', + name: 'a', typeDefs: parse(/* GraphQL */ ` extend schema @link( @@ -319,7 +356,7 @@ testImplementations(_ => { `), }, { - name: 'subgraphB', + name: 'b', typeDefs: parse(/* GraphQL */ ` extend schema @link( @@ -336,18 +373,27 @@ testImplementations(_ => { } `), }, - ]) as CompositionFailure; - expect(result.errors).toMatchInlineSnapshot(` - [ - [GraphQLError: @interfaceObject MyInterface in subgraph SUBGRAPH_B doesn't have corresponding entity interface in the different subgraph.], - ] - `); + ]); + + assertCompositionFailure(result); + + expect(result.errors).toContainEqual( + expect.objectContaining({ + message: + api.library === 'apollo' + ? `Type "MyInterface" is declared with @interfaceObject in all the subgraphs in which is is defined (it is defined in subgraph "b" but should be defined as an interface in at least one subgraph)` + : 'Type "MyInterface" is declared with @interfaceObject in all the subgraphs in which is is defined', + extensions: expect.objectContaining({ + code: 'INTERFACE_OBJECT_USAGE_ERROR', + }), + }), + ); }); test(`interface type is present on other subgraph but doesn't have @key directive. Should fail`, () => { - const result = composeServices([ + const result = api.composeServices([ { - name: 'subgraphA', + name: 'a', typeDefs: parse(/* GraphQL */ ` extend schema @link( @@ -371,7 +417,7 @@ testImplementations(_ => { `), }, { - name: 'subgraphB', + name: 'b', typeDefs: parse(/* GraphQL */ ` extend schema @link( @@ -388,18 +434,24 @@ testImplementations(_ => { } `), }, - ]) as CompositionFailure; - expect(result.errors).toMatchInlineSnapshot(` - [ - [GraphQLError: @key directive must be present on interface type MyInterface in subgraph SUBGRAPH_A for @objectInterface to work], - ] - `); + ]); + + assertCompositionFailure(result); + expect(result.errors).toContainEqual( + expect.objectContaining({ + message: + '[b] The @interfaceObject directive can only be applied to entity types but type "MyInterface" has no @key in this subgraph.', + extensions: expect.objectContaining({ + code: 'INTERFACE_OBJECT_USAGE_ERROR', + }), + }), + ); }); test(`interface type is present on other subgraph with @key directive. Should succeed and add fields from interfaceObject`, () => { - const result = composeServices([ + const result = api.composeServices([ { - name: 'subgraphA', + name: 'a', typeDefs: parse(/* GraphQL */ ` extend schema @link( @@ -423,7 +475,7 @@ testImplementations(_ => { `), }, { - name: 'subgraphB', + name: 'b', typeDefs: parse(/* GraphQL */ ` extend schema @link( @@ -440,12 +492,14 @@ testImplementations(_ => { } `), }, - ]) as CompositionSuccess; + ]); + + assertCompositionSuccess(result); expect(result.supergraphSdl).toContainGraphQL(/* GraphQL */ ` type IimplementMyInterface implements MyInterface - @join__type(graph: SUBGRAPH_A, key: "id") - @join__implements(graph: SUBGRAPH_A, interface: "MyInterface") { + @join__type(graph: A, key: "id") + @join__implements(graph: A, interface: "MyInterface") { id: ID! field: String hello: Int @join__field @@ -453,11 +507,11 @@ testImplementations(_ => { `); expect(result.supergraphSdl).toContainGraphQL(/* GraphQL */ ` interface MyInterface - @join__type(graph: SUBGRAPH_A, key: "id") - @join__type(graph: SUBGRAPH_B, isInterfaceObject: true, key: "id") { + @join__type(graph: A, key: "id") + @join__type(graph: B, isInterfaceObject: true, key: "id") { id: ID! - field: String @join__field(graph: SUBGRAPH_A) - hello: Int @join__field(graph: SUBGRAPH_B) + field: String @join__field(graph: A) + hello: Int @join__field(graph: B) } `); }); @@ -465,9 +519,9 @@ testImplementations(_ => { describe(`fields contribution`, () => { test(`several subgraphs contribute fields to the same interface through interfaceObject. Should succeed`, () => { - const result = composeServices([ + const result = api.composeServices([ { - name: 'subgraphA', + name: 'a', typeDefs: parse(/* GraphQL */ ` extend schema @link( @@ -491,7 +545,7 @@ testImplementations(_ => { `), }, { - name: 'subgraphB', + name: 'b', typeDefs: parse(/* GraphQL */ ` extend schema @link( @@ -509,7 +563,7 @@ testImplementations(_ => { `), }, { - name: 'subgraphC', + name: 'c', typeDefs: parse(/* GraphQL */ ` extend schema @link( @@ -526,12 +580,14 @@ testImplementations(_ => { } `), }, - ]) as CompositionSuccess; + ]); + + assertCompositionSuccess(result); expect(result.supergraphSdl).toContainGraphQL(/* GraphQL */ ` type IimplementMyInterface implements MyInterface - @join__type(graph: SUBGRAPH_A, key: "id") - @join__implements(graph: SUBGRAPH_A, interface: "MyInterface") { + @join__type(graph: A, key: "id") + @join__implements(graph: A, interface: "MyInterface") { id: ID! field: String hello: Int @join__field @@ -540,13 +596,13 @@ testImplementations(_ => { `); expect(result.supergraphSdl).toContainGraphQL(/* GraphQL */ ` interface MyInterface - @join__type(graph: SUBGRAPH_A, key: "id") - @join__type(graph: SUBGRAPH_B, isInterfaceObject: true, key: "id") - @join__type(graph: SUBGRAPH_C, isInterfaceObject: true, key: "id") { + @join__type(graph: A, key: "id") + @join__type(graph: B, isInterfaceObject: true, key: "id") + @join__type(graph: C, isInterfaceObject: true, key: "id") { id: ID! - field: String @join__field(graph: SUBGRAPH_A) - hello: Int @join__field(graph: SUBGRAPH_B) - hello2: Int @join__field(graph: SUBGRAPH_C) + field: String @join__field(graph: A) + hello: Int @join__field(graph: B) + hello2: Int @join__field(graph: C) } `); }); diff --git a/__tests__/shared/testkit.ts b/__tests__/shared/testkit.ts index 242e7f9..f4d8806 100644 --- a/__tests__/shared/testkit.ts +++ b/__tests__/shared/testkit.ts @@ -19,14 +19,12 @@ export function normalizeErrorMessage(literals: string | readonly string[]) { const missingErrorCodes = [ 'DISALLOWED_INACCESSIBLE', - 'DOWNSTREAM_SERVICE_ERROR', 'EXTERNAL_ARGUMENT_DEFAULT_MISMATCH', 'EXTERNAL_ARGUMENT_TYPE_MISMATCH', 'EXTERNAL_COLLISION_WITH_ANOTHER_DIRECTIVE', 'IMPLEMENTED_BY_INACCESSIBLE', 'INVALID_FEDERATION_SUPERGRAPH', 'LINK_IMPORT_NAME_MISMATCH', - 'REQUIRED_INACCESSIBLE', 'SHAREABLE_HAS_MISMATCHED_RUNTIME_TYPES', 'UNSUPPORTED_FEATURE', 'UNSUPPORTED_LINKED_FEATURE', diff --git a/__tests__/subgraph/errors/DIRECTIVE_DEFINITION_INVALID.spec.ts b/__tests__/subgraph/errors/DIRECTIVE_DEFINITION_INVALID.spec.ts index e6ec94f..4494454 100644 --- a/__tests__/subgraph/errors/DIRECTIVE_DEFINITION_INVALID.spec.ts +++ b/__tests__/subgraph/errors/DIRECTIVE_DEFINITION_INVALID.spec.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from 'vitest'; import { + assertCompositionFailure, assertCompositionSuccess, createStarsStuff, graphql, @@ -49,49 +50,59 @@ testVersions((api, version) => { }, ]), ); + }); - test('different location should be ignored if imported but not used', () => { - assertCompositionSuccess( - api.composeServices([ - { - name: 'users', - typeDefs: graphql` - extend schema - @link(url: "https://specs.apollo.dev/federation/${version}", import: ["@key"]) - - type User @key(fields: "id") { - id: ID! - } - - type Query { - users: [User] - } - `, - }, - { - name: 'profiles', - typeDefs: graphql` - extend schema - @link( - url: "https://specs.apollo.dev/federation/${version}" - import: ["@key", "@external", "@extends"] - ) - - directive @extends on FIELD_DEFINITION - - type User @key(fields: "id") { - id: ID! - profile: Profile - } - - type Profile { - name: String! - } - `, - }, - ]), - ); - }); + test('different location should be ignored if imported but not used', () => { + const result = api.composeServices([ + { + name: 'users', + typeDefs: graphql` + extend schema + @link(url: "https://specs.apollo.dev/federation/${version}", import: ["@key"]) + + type User @key(fields: "id") { + id: ID! + } + + type Query { + users: [User] + } + `, + }, + { + name: 'profiles', + typeDefs: graphql` + extend schema + @link( + url: "https://specs.apollo.dev/federation/${version}" + import: ["@key", "@external", "@extends"] + ) + + directive @extends on FIELD_DEFINITION + + type User @key(fields: "id") { + id: ID! + profile: Profile + } + + type Profile { + name: String! + } + `, + }, + ]); + + assertCompositionFailure(result); + + expect(result.errors).toContainEqual( + expect.objectContaining({ + message: + '[profiles] Invalid definition for directive "@extends": "@extends" should have locations OBJECT, INTERFACE, but found (non-subset) FIELD_DEFINITION', + extensions: expect.objectContaining({ + code: 'DIRECTIVE_DEFINITION_INVALID', + }), + }), + ); }); }); diff --git a/__tests__/supergraph/errors/INTERFACE_KEY_MISSING_IMPLEMENTATION_TYPE.spec.ts b/__tests__/supergraph/errors/INTERFACE_KEY_MISSING_IMPLEMENTATION_TYPE.spec.ts index 13d6d35..c71de88 100644 --- a/__tests__/supergraph/errors/INTERFACE_KEY_MISSING_IMPLEMENTATION_TYPE.spec.ts +++ b/__tests__/supergraph/errors/INTERFACE_KEY_MISSING_IMPLEMENTATION_TYPE.spec.ts @@ -1,5 +1,10 @@ import { expect, test } from 'vitest'; -import { graphql, satisfiesVersionRange, testVersions } from '../../shared/testkit.js'; +import { + assertCompositionSuccess, + graphql, + satisfiesVersionRange, + testVersions, +} from '../../shared/testkit.js'; testVersions((api, version) => { test('INTERFACE_KEY_MISSING_IMPLEMENTATION_TYPE', () => { @@ -103,6 +108,41 @@ testVersions((api, version) => { ), }), ); + + assertCompositionSuccess( + api.composeServices([ + { + name: 'a', + typeDefs: graphql` + type Query { + a: String + } + + extend interface Node @key(fields: "id") { + id: ID! + name: String + } + `, + }, + { + name: 'b', + typeDefs: graphql` + type Query { + b: String + } + + interface Node { + id: ID! + } + + type User implements Node @key(fields: "id") { + id: ID! + name: String + } + `, + }, + ]), + ); }); test('INTERFACE_KEY_MISSING_IMPLEMENTATION_TYPE: multiple keys', () => { @@ -207,4 +247,47 @@ testVersions((api, version) => { }), ); }); + + test('INTERFACE_KEY_MISSING_IMPLEMENTATION_TYPE: @interfaceObject + interface is valid', () => { + assertCompositionSuccess( + api.composeServices([ + { + name: 'a', + typeDefs: graphql` + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key", "@interfaceObject"] + ) + + interface Node @key(fields: "id") { + id: ID! + } + + type Query { + a: String + } + `, + }, + { + name: 'b', + typeDefs: graphql` + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key", "@interfaceObject"] + ) + + type Node @key(fields: "id", resolvable: false) @interfaceObject { + id: ID! + } + + type Query { + b: String + } + `, + }, + ]), + ); + }); }); diff --git a/__tests__/supergraph/errors/INTERFACE_OBJECT_USAGE_ERROR.spec.ts b/__tests__/supergraph/errors/INTERFACE_OBJECT_USAGE_ERROR.spec.ts index c4a801a..9782860 100644 --- a/__tests__/supergraph/errors/INTERFACE_OBJECT_USAGE_ERROR.spec.ts +++ b/__tests__/supergraph/errors/INTERFACE_OBJECT_USAGE_ERROR.spec.ts @@ -2,7 +2,7 @@ import { expect, test } from 'vitest'; import { graphql, satisfiesVersionRange, testVersions } from '../../shared/testkit.js'; testVersions((api, version) => { - test.skipIf(api.library === 'guild')('INTERFACE_OBJECT_USAGE_ERROR', () => { + test('INTERFACE_OBJECT_USAGE_ERROR', () => { expect( api.composeServices([ { diff --git a/__tests__/supergraph/errors/INVALID_FIELD_SHARING.spec.ts b/__tests__/supergraph/errors/INVALID_FIELD_SHARING.spec.ts index 46e63c7..824b7bd 100644 --- a/__tests__/supergraph/errors/INVALID_FIELD_SHARING.spec.ts +++ b/__tests__/supergraph/errors/INVALID_FIELD_SHARING.spec.ts @@ -1,5 +1,10 @@ import { describe, expect, test } from 'vitest'; -import { assertCompositionSuccess, graphql, testVersions } from '../../shared/testkit.js'; +import { + assertCompositionFailure, + assertCompositionSuccess, + graphql, + testVersions, +} from '../../shared/testkit.js'; testVersions((api, version) => { describe('INVALID_FIELD_SHARING', () => { @@ -444,4 +449,75 @@ testVersions((api, version) => { ); }); }); + + test('non-shareable field in @interfaceObject and interface implementation', () => { + const result = api.composeServices([ + { + name: 'a', + typeDefs: graphql` + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key", "@interfaceObject", "@shareable"] + ) + + type Query { + hello: String + } + + interface MyInterface @key(fields: "id") { + id: ID! + field: String + } + + type MyType implements MyInterface @key(fields: "id") { + id: ID! + field: String + } + `, + }, + { + name: 'b', + typeDefs: graphql` + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key", "@interfaceObject"] + ) + + type MyInterface @key(fields: "id") @interfaceObject { + id: ID! + newField: String + } + `, + }, + { + name: 'c', + typeDefs: graphql` + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key", "@interfaceObject"] + ) + + type MyInterface @key(fields: "id", resolvable: false) @interfaceObject { + id: ID! + field: String + } + `, + }, + ]); + + assertCompositionFailure(result); + + expect(result.errors).toContainEqual( + expect.objectContaining({ + message: + 'Non-shareable field "MyType.field" is resolved from multiple subgraphs: it is resolved from subgraphs "a" and "c" (through @interfaceObject field "MyInterface.field") and defined as non-shareable in all of them', + extensions: expect.objectContaining({ + code: 'INVALID_FIELD_SHARING', + }), + }), + ); + }); }); diff --git a/__tests__/supergraph/errors/ONLY_INACCESSIBLE_CHILDREN.spec.ts b/__tests__/supergraph/errors/ONLY_INACCESSIBLE_CHILDREN.spec.ts index 27ba11e..3a23493 100644 --- a/__tests__/supergraph/errors/ONLY_INACCESSIBLE_CHILDREN.spec.ts +++ b/__tests__/supergraph/errors/ONLY_INACCESSIBLE_CHILDREN.spec.ts @@ -204,5 +204,59 @@ testVersions((api, version) => { }, ]), ); + + expect( + api.composeServices([ + { + name: 'users', + typeDefs: graphql` + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key", "@inaccessible"] + ) + + type Query { + users: [User] + } + + interface Node @key(fields: "id") { + id: ID @inaccessible + } + + type User implements Node @key(fields: "id") { + id: ID! + } + `, + }, + { + name: 'products', + typeDefs: graphql` + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key", "@interfaceObject"] + ) + + type Node @key(fields: "id", resolvable: false) @interfaceObject { + id: ID + } + `, + }, + ]), + ).toEqual( + expect.objectContaining({ + errors: expect.arrayContaining([ + expect.objectContaining({ + message: expect.stringContaining( + `Type "Node" is in the API schema but all of its fields are @inaccessible.`, + ), + extensions: expect.objectContaining({ + code: 'ONLY_INACCESSIBLE_CHILDREN', + }), + }), + ]), + }), + ); }); }); diff --git a/__tests__/supergraph/errors/SATISFIABILITY_ERROR.spec.ts b/__tests__/supergraph/errors/SATISFIABILITY_ERROR.spec.ts index bcebb08..b40154b 100644 --- a/__tests__/supergraph/errors/SATISFIABILITY_ERROR.spec.ts +++ b/__tests__/supergraph/errors/SATISFIABILITY_ERROR.spec.ts @@ -1489,7 +1489,6 @@ testVersions((api, version) => { }); // ADD IT TO THE COLLECTION - // TODO: maximum call stack size exceeded test('external but somehow resolvable', () => { assertCompositionSuccess( api.composeServices([ @@ -3504,4 +3503,294 @@ testVersions((api, version) => { ); expect(result.errors).toHaveLength(1); }); + + test('@require in interfaceObject', () => { + const result = api.composeServices([ + { + name: 'a', + typeDefs: graphql` + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key", "@interfaceObject"] + ) + + type Product @key(fields: "id") @interfaceObject { + id: String! + category: Category! + } + + type Category @key(fields: "id") { + id: ID! + name: String! + } + `, + }, + { + name: 'b', + typeDefs: graphql` + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key", "@interfaceObject", "@requires", "@external"] + ) + + type Product @key(fields: "id") @interfaceObject { + id: String! + categoryName: String @requires(fields: "category { name }") + category: Category! @external + } + + type Category { + name: String! @external + } + `, + }, + { + name: 'c', + typeDefs: graphql` + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key", "@interfaceObject"] + ) + + interface Product @key(fields: "id") { + id: String! + } + + type Tile implements Product @key(fields: "id") { + id: String! + size: Int! + } + + type Query { + tiles: [Tile!]! + } + `, + }, + ]); + + assertCompositionSuccess(result); + }); + + test('deeply nested interfaces with @requires(fields: )', () => { + assertCompositionSuccess( + api.composeServices([ + { + name: 'a', + typeDefs: graphql` + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key", "@external", "@requires"] + ) + + interface Product { + id: ID! + } + + interface Location { + id: ID! + } + + type Warsaw implements Location @key(fields: "id") { + id: ID! + products: [Product!]! @external + } + + type Lidl @key(fields: "id") { + id: ID! + locations: [Location!] @external + products(location: ID): [Product]! + @requires( + fields: """ + locations { + ... on Warsaw { + products { + id + } + } + } + """ + ) + } + `, + }, + { + name: 'b', + typeDefs: graphql` + extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"]) + + type Query { + products: [Product]! + } + + interface Shop @key(fields: "id") { + id: ID! + } + + interface Location @key(fields: "id") { + id: ID! + } + + type Lidl implements Shop @key(fields: "id") { + id: ID! + locations: [Location!] + } + + type Warsaw implements Location @key(fields: "id") { + id: ID! + products: [Product!]! + } + + interface Product @key(fields: "id") { + id: ID! + } + + type Bread implements Product @key(fields: "id") { + id: ID! + shops: [Shop!]! + } + `, + }, + ]), + ); + }); + + test('unreachable interface implementation (@interfaceObject) - interface lacks @key', () => { + // This is a valid composition, as the interface is annotated with @key + assertCompositionSuccess( + api.composeServices([ + { + name: 'a', + typeDefs: graphql` + extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"]) + + interface IData @key(fields: "id") { + id: String + } + + type Data implements IData @key(fields: "id") { + id: String + } + `, + }, + { + name: 'b', + typeDefs: graphql` + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key", "@interfaceObject"] + ) + + type Query { + idata: IData + } + + type IData @interfaceObject @key(fields: "id") { + id: String + } + `, + }, + ]), + ); + + // This is an invalid composition, as the interface is NOT annotated with @key + const result = api.composeServices([ + { + name: 'a', + typeDefs: graphql` + extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"]) + + interface IData { + id: String + } + + type Data implements IData @key(fields: "id") { + id: String + } + `, + }, + { + name: 'b', + typeDefs: graphql` + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key", "@interfaceObject"] + ) + + type Query { + idata: IData + } + + type IData @interfaceObject @key(fields: "id") { + id: String + } + `, + }, + ]); + assertCompositionFailure(result); + + expect(result.errors).toContainEqual( + expect.objectContaining({ + message: expect.stringMatching(normalizeErrorMessage` + The following supergraph API query: + { + idata { + ... on Data { + ... + } + } + } + cannot be satisfied by the subgraphs because: + - from subgraph "b": no subgraph can be reached to resolve the implementation type of @interfaceObject type "IData".`), + extensions: expect.objectContaining({ + code: 'SATISFIABILITY_ERROR', + }), + }), + ); + }); + + test('make sure moving from @interfaceObject to an implementation of the interface is allowed', () => { + assertCompositionSuccess( + api.composeServices([ + { + name: 'a', + typeDefs: graphql` + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key", "@interfaceObject"] + ) + + type Query { + products: Product! + } + + type Product @key(fields: "id", resolvable: false) @interfaceObject { + id: ID! + } + `, + }, + { + name: 'b', + typeDefs: graphql` + extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"]) + + interface Product @key(fields: "id") { + id: ID! + } + + type Bread implements Product @key(fields: "id") { + id: ID! + name: String! + } + `, + }, + ]), + ); + }); }); diff --git a/compose.ts b/compose.ts new file mode 100644 index 0000000..da0b90a --- /dev/null +++ b/compose.ts @@ -0,0 +1,74 @@ +import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { basename, join } from 'node:path'; +import { DocumentNode, parse, print, Source, visit } from 'graphql'; +import { composeServices as apolloComposeServices } from '@apollo/composition'; +import { getSubgraphs as getDGS } from './__tests__/fixtures/dgs/index.js'; +import { getSubgraphs as getHuge } from './__tests__/fixtures/huge-schema/index.js'; +import { composeServices as guildComposeServices } from './src/index.js'; + +const args = process.argv.slice(2); +const isApollo = args.includes('apollo'); +const composeServices = isApollo ? apolloComposeServices : guildComposeServices; + +function fromDirectory(directoryName: string) { + const filepaths = readdirSync(directoryName); + return filepaths + .filter(f => f.endsWith('.graphql')) + .map(f => { + const originalNameSourceFile = join(directoryName, f.replace('.graphql', '.log')); + let name = basename(f).replace('.graphql', '').replace('_', '-'); + + if (existsSync(originalNameSourceFile)) { + name = readFileSync(originalNameSourceFile, 'utf-8'); + } + + const typeDefs = visit(parse(new Source(readFileSync(join(directoryName, f), 'utf-8'), f)), { + enter(node) { + if ('description' in node) { + return { + ...node, + description: undefined, + }; + } + }, + }); + + writeFileSync(join(directoryName, f), print(typeDefs)); + + return { + name, + typeDefs, + }; + }); +} + +let services: Array<{ + typeDefs: DocumentNode; + name: string; +}> = []; +// services = await getDGS(); +// services = await getHuge(); +services = fromDirectory('./temp'); + +if (typeof gc === 'function') { + gc(); +} + +debugger; + +console.time('Total'); +console.log('Composing', services.length, 'services'); +const result = composeServices(services); +console.timeEnd('Total'); + +debugger; + +const memoryAfter = process.memoryUsage().heapUsed; + +console.log('Memory:', memoryAfter / 1024 / 1024, 'MB'); +const hasErrors = 'errors' in result && result.errors && result.errors.length; +console.log(hasErrors ? '❌ Failed' : '✅ Succeeded'); + +if (hasErrors) { + console.log(result.errors.map(e => (e.extensions.code ?? '') + ' ' + e.message).join('\n\n')); +} diff --git a/package.json b/package.json index 442ce1c..2ac597e 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,10 @@ "scripts": { "bench": "tsx ./benchmark.ts", "build": "rimraf dist && bob build && bob check", + "compose": "tsx --expose-gc ./compose.ts", + "compose:apollo": "pnpm compose apollo", + "compose:debug": "DEBUG_COLORS=0 DEBUG=composition:* pnpm compose > guild.log", + "compose:inspect": "tsx --inspect-brk --expose-gc ./compose.ts", "format": "prettier --write .", "release": "pnpm build && changeset publish", "test": "vitest --config ./vitest.config.js", @@ -69,19 +73,19 @@ "@changesets/cli": "2.26.2", "@theguild/prettier-config": "2.0.1", "@types/debug": "4.1.12", - "@types/lodash.sortby": "4.7.7", + "@types/lodash.sortby": "4.7.9", "@types/node": "20.6.0", - "@vitest/ui": "0.34.4", + "@vitest/ui": "1.4.0", "bob-the-bundler": "7.0.1", - "graphql": "16.8.0", + "graphql": "16.8.1", "lodash.sortby": "4.7.0", "mitata": "0.1.6", - "prettier": "3.0.3", - "rimraf": "5.0.1", + "prettier": "3.2.5", + "rimraf": "5.0.5", "strip-indent": "4.0.0", - "tsx": "3.12.8", - "typescript": "5.2.2", - "vitest": "0.34.4" + "tsx": "4.7.1", + "typescript": "5.4.2", + "vitest": "1.4.0" }, "publishConfig": { "directory": "dist", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1f07f5a..efad8cf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,7 +21,7 @@ dependencies: devDependencies: '@apollo/composition': specifier: 2.6.2 - version: 2.6.2(graphql@16.8.0) + version: 2.6.2(graphql@16.8.1) '@changesets/changelog-github': specifier: 0.4.8 version: 0.4.8 @@ -30,46 +30,46 @@ devDependencies: version: 2.26.2 '@theguild/prettier-config': specifier: 2.0.1 - version: 2.0.1(prettier@3.0.3) + version: 2.0.1(prettier@3.2.5) '@types/debug': specifier: 4.1.12 version: 4.1.12 '@types/lodash.sortby': - specifier: 4.7.7 - version: 4.7.7 + specifier: 4.7.9 + version: 4.7.9 '@types/node': specifier: 20.6.0 version: 20.6.0 '@vitest/ui': - specifier: 0.34.4 - version: 0.34.4(vitest@0.34.4) + specifier: 1.4.0 + version: 1.4.0(vitest@1.4.0) bob-the-bundler: specifier: 7.0.1 - version: 7.0.1(typescript@5.2.2) + version: 7.0.1(typescript@5.4.2) graphql: - specifier: 16.8.0 - version: 16.8.0 + specifier: 16.8.1 + version: 16.8.1 mitata: specifier: 0.1.6 version: 0.1.6 prettier: - specifier: 3.0.3 - version: 3.0.3 + specifier: 3.2.5 + version: 3.2.5 rimraf: - specifier: 5.0.1 - version: 5.0.1 + specifier: 5.0.5 + version: 5.0.5 strip-indent: specifier: 4.0.0 version: 4.0.0 tsx: - specifier: 3.12.8 - version: 3.12.8 + specifier: 4.7.1 + version: 4.7.1 typescript: - specifier: 5.2.2 - version: 5.2.2 + specifier: 5.4.2 + version: 5.4.2 vitest: - specifier: 0.34.4 - version: 0.34.4(@vitest/ui@0.34.4) + specifier: 1.4.0 + version: 1.4.0(@types/node@20.6.0)(@vitest/ui@1.4.0) packages: @@ -81,18 +81,18 @@ packages: '@jridgewell/trace-mapping': 0.3.19 dev: true - /@apollo/composition@2.6.2(graphql@16.8.0): + /@apollo/composition@2.6.2(graphql@16.8.1): resolution: {integrity: sha512-rbMRcnvEDtOT+e6OiKUY9virbhpIK9E8bQ6eGoakwCYz8bVXdBBPQdKuvhnvzNAe0LeOPMcsx3Pp9mPT9wz80g==} engines: {node: '>=14.15.0'} peerDependencies: graphql: ^16.5.0 dependencies: - '@apollo/federation-internals': 2.6.2(graphql@16.8.0) - '@apollo/query-graphs': 2.6.2(graphql@16.8.0) - graphql: 16.8.0 + '@apollo/federation-internals': 2.6.2(graphql@16.8.1) + '@apollo/query-graphs': 2.6.2(graphql@16.8.1) + graphql: 16.8.1 dev: true - /@apollo/federation-internals@2.6.2(graphql@16.8.0): + /@apollo/federation-internals@2.6.2(graphql@16.8.1): resolution: {integrity: sha512-L5Ppl+FQ2+ETpJ8NCa7T8ifAjAX8K/4NW8N08d6TRUJu0M/8rvIL0CgX033Jno/+FVIFhNBbVN2kGoSKDl1YPQ==} engines: {node: '>=14.15.0'} peerDependencies: @@ -100,20 +100,20 @@ packages: dependencies: '@types/uuid': 9.0.3 chalk: 4.1.2 - graphql: 16.8.0 + graphql: 16.8.1 js-levenshtein: 1.1.6 uuid: 9.0.0 dev: true - /@apollo/query-graphs@2.6.2(graphql@16.8.0): + /@apollo/query-graphs@2.6.2(graphql@16.8.1): resolution: {integrity: sha512-1pQABsPS38Sqz1u3pW1WDmq/xJDWkdZGUsHNSAaSbdRAQMT5Lf9M9uzBUcNR5g+byvzKOc6nnxkh2W/tls5kBw==} engines: {node: '>=14.15.0'} peerDependencies: graphql: ^16.5.0 dependencies: - '@apollo/federation-internals': 2.6.2(graphql@16.8.0) + '@apollo/federation-internals': 2.6.2(graphql@16.8.1) deep-equal: 2.2.2 - graphql: 16.8.0 + graphql: 16.8.1 ts-graphviz: 1.8.1 uuid: 9.0.0 dev: true @@ -519,29 +519,17 @@ packages: prettier: 2.8.8 dev: true - /@esbuild-kit/cjs-loader@2.4.2: - resolution: {integrity: sha512-BDXFbYOJzT/NBEtp71cvsrGPwGAMGRB/349rwKuoxNSiKjPraNNnlK6MIIabViCjqZugu6j+xeMDlEkWdHHJSg==} - dependencies: - '@esbuild-kit/core-utils': 3.2.3 - get-tsconfig: 4.7.0 - dev: true - - /@esbuild-kit/core-utils@3.2.3: - resolution: {integrity: sha512-daTXIDXv6lTpADQYTfY9BgHfL3HjVfY7xD8aOuPWtD1bJdqk9hAtHK5cErBcxcBtjFcfB0B2tftFtW01jTveKw==} - dependencies: - esbuild: 0.18.20 - source-map-support: 0.5.21 - dev: true - - /@esbuild-kit/esm-loader@2.5.5: - resolution: {integrity: sha512-Qwfvj/qoPbClxCRNuac1Du01r9gvNOT+pMYtJDapfB1eoGN1YlJ1BixLyL9WVENRx5RXgNLdfYdx/CuswlGhMw==} - dependencies: - '@esbuild-kit/core-utils': 3.2.3 - get-tsconfig: 4.7.0 + /@esbuild/aix-ppc64@0.19.12: + resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + requiresBuild: true dev: true + optional: true - /@esbuild/android-arm64@0.18.20: - resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + /@esbuild/android-arm64@0.19.12: + resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} engines: {node: '>=12'} cpu: [arm64] os: [android] @@ -549,8 +537,8 @@ packages: dev: true optional: true - /@esbuild/android-arm@0.18.20: - resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + /@esbuild/android-arm@0.19.12: + resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} engines: {node: '>=12'} cpu: [arm] os: [android] @@ -558,8 +546,8 @@ packages: dev: true optional: true - /@esbuild/android-x64@0.18.20: - resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + /@esbuild/android-x64@0.19.12: + resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} engines: {node: '>=12'} cpu: [x64] os: [android] @@ -567,8 +555,8 @@ packages: dev: true optional: true - /@esbuild/darwin-arm64@0.18.20: - resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + /@esbuild/darwin-arm64@0.19.12: + resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] @@ -576,8 +564,8 @@ packages: dev: true optional: true - /@esbuild/darwin-x64@0.18.20: - resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + /@esbuild/darwin-x64@0.19.12: + resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} engines: {node: '>=12'} cpu: [x64] os: [darwin] @@ -585,8 +573,8 @@ packages: dev: true optional: true - /@esbuild/freebsd-arm64@0.18.20: - resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + /@esbuild/freebsd-arm64@0.19.12: + resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] @@ -594,8 +582,8 @@ packages: dev: true optional: true - /@esbuild/freebsd-x64@0.18.20: - resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + /@esbuild/freebsd-x64@0.19.12: + resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} engines: {node: '>=12'} cpu: [x64] os: [freebsd] @@ -603,8 +591,8 @@ packages: dev: true optional: true - /@esbuild/linux-arm64@0.18.20: - resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + /@esbuild/linux-arm64@0.19.12: + resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} engines: {node: '>=12'} cpu: [arm64] os: [linux] @@ -612,8 +600,8 @@ packages: dev: true optional: true - /@esbuild/linux-arm@0.18.20: - resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + /@esbuild/linux-arm@0.19.12: + resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} engines: {node: '>=12'} cpu: [arm] os: [linux] @@ -621,8 +609,8 @@ packages: dev: true optional: true - /@esbuild/linux-ia32@0.18.20: - resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + /@esbuild/linux-ia32@0.19.12: + resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} engines: {node: '>=12'} cpu: [ia32] os: [linux] @@ -630,8 +618,8 @@ packages: dev: true optional: true - /@esbuild/linux-loong64@0.18.20: - resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + /@esbuild/linux-loong64@0.19.12: + resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} engines: {node: '>=12'} cpu: [loong64] os: [linux] @@ -639,8 +627,8 @@ packages: dev: true optional: true - /@esbuild/linux-mips64el@0.18.20: - resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + /@esbuild/linux-mips64el@0.19.12: + resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} engines: {node: '>=12'} cpu: [mips64el] os: [linux] @@ -648,8 +636,8 @@ packages: dev: true optional: true - /@esbuild/linux-ppc64@0.18.20: - resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + /@esbuild/linux-ppc64@0.19.12: + resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} engines: {node: '>=12'} cpu: [ppc64] os: [linux] @@ -657,8 +645,8 @@ packages: dev: true optional: true - /@esbuild/linux-riscv64@0.18.20: - resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + /@esbuild/linux-riscv64@0.19.12: + resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} engines: {node: '>=12'} cpu: [riscv64] os: [linux] @@ -666,8 +654,8 @@ packages: dev: true optional: true - /@esbuild/linux-s390x@0.18.20: - resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + /@esbuild/linux-s390x@0.19.12: + resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} engines: {node: '>=12'} cpu: [s390x] os: [linux] @@ -675,8 +663,8 @@ packages: dev: true optional: true - /@esbuild/linux-x64@0.18.20: - resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + /@esbuild/linux-x64@0.19.12: + resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} engines: {node: '>=12'} cpu: [x64] os: [linux] @@ -684,8 +672,8 @@ packages: dev: true optional: true - /@esbuild/netbsd-x64@0.18.20: - resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + /@esbuild/netbsd-x64@0.19.12: + resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} engines: {node: '>=12'} cpu: [x64] os: [netbsd] @@ -693,8 +681,8 @@ packages: dev: true optional: true - /@esbuild/openbsd-x64@0.18.20: - resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + /@esbuild/openbsd-x64@0.19.12: + resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} engines: {node: '>=12'} cpu: [x64] os: [openbsd] @@ -702,8 +690,8 @@ packages: dev: true optional: true - /@esbuild/sunos-x64@0.18.20: - resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + /@esbuild/sunos-x64@0.19.12: + resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} engines: {node: '>=12'} cpu: [x64] os: [sunos] @@ -711,8 +699,8 @@ packages: dev: true optional: true - /@esbuild/win32-arm64@0.18.20: - resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + /@esbuild/win32-arm64@0.19.12: + resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} engines: {node: '>=12'} cpu: [arm64] os: [win32] @@ -720,8 +708,8 @@ packages: dev: true optional: true - /@esbuild/win32-ia32@0.18.20: - resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + /@esbuild/win32-ia32@0.19.12: + resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} engines: {node: '>=12'} cpu: [ia32] os: [win32] @@ -729,8 +717,8 @@ packages: dev: true optional: true - /@esbuild/win32-x64@0.18.20: - resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + /@esbuild/win32-x64@0.19.12: + resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} engines: {node: '>=12'} cpu: [x64] os: [win32] @@ -738,7 +726,7 @@ packages: dev: true optional: true - /@ianvs/prettier-plugin-sort-imports@4.0.2(prettier@3.0.3): + /@ianvs/prettier-plugin-sort-imports@4.0.2(prettier@3.2.5): resolution: {integrity: sha512-VnsTzyb9aSWpc3v6HvZKD6eolZRvofIYjhda+6IbW1GYwr2byWqK0KhLPbYNkit9MAgShad5bhZ1hgBn867A1A==} peerDependencies: '@vue/compiler-sfc': '>=3.0.0' @@ -752,7 +740,7 @@ packages: '@babel/parser': 7.22.16 '@babel/traverse': 7.22.17 '@babel/types': 7.22.17 - prettier: 3.0.3 + prettier: 3.2.5 semver: 7.5.4 transitivePeerDependencies: - supports-color @@ -855,52 +843,150 @@ packages: dev: true optional: true - /@polka/url@1.0.0-next.23: - resolution: {integrity: sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==} + /@polka/url@1.0.0-next.25: + resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==} + dev: true + + /@rollup/rollup-android-arm-eabi@4.13.0: + resolution: {integrity: sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-android-arm64@4.13.0: + resolution: {integrity: sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-darwin-arm64@4.13.0: + resolution: {integrity: sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-darwin-x64@4.13.0: + resolution: {integrity: sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==} + cpu: [x64] + os: [darwin] + requiresBuild: true dev: true + optional: true + + /@rollup/rollup-linux-arm-gnueabihf@4.13.0: + resolution: {integrity: sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm64-gnu@4.13.0: + resolution: {integrity: sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm64-musl@4.13.0: + resolution: {integrity: sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-riscv64-gnu@4.13.0: + resolution: {integrity: sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-x64-gnu@4.13.0: + resolution: {integrity: sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-x64-musl@4.13.0: + resolution: {integrity: sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-arm64-msvc@4.13.0: + resolution: {integrity: sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-ia32-msvc@4.13.0: + resolution: {integrity: sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-x64-msvc@4.13.0: + resolution: {integrity: sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true /@sinclair/typebox@0.27.8: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} dev: true - /@theguild/prettier-config@2.0.1(prettier@3.0.3): + /@theguild/prettier-config@2.0.1(prettier@3.2.5): resolution: {integrity: sha512-I2rwFhQv8/nuepfbPwpja5Ia+ONs6DIvV3W6DLzO4609sDiOG4FUaNBiryo7mYbWKN8XBLUNLwOUT2GaOBC2Iw==} peerDependencies: prettier: ^3 dependencies: - '@ianvs/prettier-plugin-sort-imports': 4.0.2(prettier@3.0.3) - prettier: 3.0.3 - prettier-plugin-pkg: 0.18.0(prettier@3.0.3) - prettier-plugin-sh: 0.13.1(prettier@3.0.3) + '@ianvs/prettier-plugin-sort-imports': 4.0.2(prettier@3.2.5) + prettier: 3.2.5 + prettier-plugin-pkg: 0.18.0(prettier@3.2.5) + prettier-plugin-sh: 0.13.1(prettier@3.2.5) transitivePeerDependencies: - '@vue/compiler-sfc' - supports-color dev: true - /@types/chai-subset@1.3.3: - resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==} - dependencies: - '@types/chai': 4.3.6 - dev: true - - /@types/chai@4.3.6: - resolution: {integrity: sha512-VOVRLM1mBxIRxydiViqPcKn6MIxZytrbMpd6RJLIWKxUNr3zux8no0Oc7kJx0WAPIitgZ0gkrDS+btlqQpubpw==} - dev: true - /@types/debug@4.1.12: resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} dependencies: '@types/ms': 0.7.34 dev: true + /@types/estree@1.0.5: + resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + dev: true + /@types/is-ci@3.0.0: resolution: {integrity: sha512-Q0Op0hdWbYd1iahB+IFNQcWXFq4O0Q5MwQP7uN0souuQ4rPg1vEYcnIOfr1gY+M+6rc8FGoRaBO1mOOvL29sEQ==} dependencies: ci-info: 3.8.0 dev: true - /@types/lodash.sortby@4.7.7: - resolution: {integrity: sha512-J/4IS+jQopGBrrRetBXDCX0KnSeXJZ0rOTmGAxR9MWGV24YdHxX8IRi9LCGAU9GKWlBov9KRSfQpuup9PReqrw==} + /@types/lodash.sortby@4.7.9: + resolution: {integrity: sha512-PDmjHnOlndLS59GofH0pnxIs+n9i4CWeXGErSB5JyNFHu2cmvW6mQOaUKjG8EDPkni14IgF8NsRW8bKvFzTm9A==} dependencies: '@types/lodash': 4.14.198 dev: true @@ -937,61 +1023,62 @@ packages: resolution: {integrity: sha512-taHQQH/3ZyI3zP8M/puluDEIEvtQHVYcC6y3N8ijFtAd28+Ey/G4sg1u2gB01S8MwybLOKAp9/yCMu/uR5l3Ug==} dev: true - /@vitest/expect@0.34.4: - resolution: {integrity: sha512-XlMKX8HyYUqB8dsY8Xxrc64J2Qs9pKMt2Z8vFTL4mBWXJsg4yoALHzJfDWi8h5nkO4Zua4zjqtapQ/IluVkSnA==} + /@vitest/expect@1.4.0: + resolution: {integrity: sha512-Jths0sWCJZ8BxjKe+p+eKsoqev1/T8lYcrjavEaz8auEJ4jAVY0GwW3JKmdVU4mmNPLPHixh4GNXP7GFtAiDHA==} dependencies: - '@vitest/spy': 0.34.4 - '@vitest/utils': 0.34.4 - chai: 4.3.8 + '@vitest/spy': 1.4.0 + '@vitest/utils': 1.4.0 + chai: 4.4.1 dev: true - /@vitest/runner@0.34.4: - resolution: {integrity: sha512-hwwdB1StERqUls8oV8YcpmTIpVeJMe4WgYuDongVzixl5hlYLT2G8afhcdADeDeqCaAmZcSgLTLtqkjPQF7x+w==} + /@vitest/runner@1.4.0: + resolution: {integrity: sha512-EDYVSmesqlQ4RD2VvWo3hQgTJ7ZrFQ2VSJdfiJiArkCerDAGeyF1i6dHkmySqk573jLp6d/cfqCN+7wUB5tLgg==} dependencies: - '@vitest/utils': 0.34.4 - p-limit: 4.0.0 + '@vitest/utils': 1.4.0 + p-limit: 5.0.0 pathe: 1.1.1 dev: true - /@vitest/snapshot@0.34.4: - resolution: {integrity: sha512-GCsh4coc3YUSL/o+BPUo7lHQbzpdttTxL6f4q0jRx2qVGoYz/cyTRDJHbnwks6TILi6560bVWoBpYC10PuTLHw==} + /@vitest/snapshot@1.4.0: + resolution: {integrity: sha512-saAFnt5pPIA5qDGxOHxJ/XxhMFKkUSBJmVt5VgDsAqPTX6JP326r5C/c9UuCMPoXNzuudTPsYDZCoJ5ilpqG2A==} dependencies: - magic-string: 0.30.3 + magic-string: 0.30.8 pathe: 1.1.1 - pretty-format: 29.6.3 + pretty-format: 29.7.0 dev: true - /@vitest/spy@0.34.4: - resolution: {integrity: sha512-PNU+fd7DUPgA3Ya924b1qKuQkonAW6hL7YUjkON3wmBwSTIlhOSpy04SJ0NrRsEbrXgMMj6Morh04BMf8k+w0g==} + /@vitest/spy@1.4.0: + resolution: {integrity: sha512-Ywau/Qs1DzM/8Uc+yA77CwSegizMlcgTJuYGAi0jujOteJOUf1ujunHThYo243KG9nAyWT3L9ifPYZ5+As/+6Q==} dependencies: - tinyspy: 2.1.1 + tinyspy: 2.2.1 dev: true - /@vitest/ui@0.34.4(vitest@0.34.4): - resolution: {integrity: sha512-gz0m0r9ErFG32r+DRdwuLJpUDpbi+jrMcw9nJZAau48Fs4LDIBg561PvczvGqyYxzbyFU7vgkSnSlSDfK0d53w==} + /@vitest/ui@1.4.0(vitest@1.4.0): + resolution: {integrity: sha512-XC6CMhN1gzYcGbpn6/Oanj4Au2EXwQEX6vpcOeLlZv8dy7g11Ukx8zwtYQbwxs9duK2s9j2o5rbQiCP5DPAcmw==} peerDependencies: - vitest: '>=0.30.1 <1' + vitest: 1.4.0 dependencies: - '@vitest/utils': 0.34.4 - fast-glob: 3.3.1 - fflate: 0.8.0 - flatted: 3.2.7 + '@vitest/utils': 1.4.0 + fast-glob: 3.3.2 + fflate: 0.8.2 + flatted: 3.3.1 pathe: 1.1.1 picocolors: 1.0.0 - sirv: 2.0.3 - vitest: 0.34.4(@vitest/ui@0.34.4) + sirv: 2.0.4 + vitest: 1.4.0(@types/node@20.6.0)(@vitest/ui@1.4.0) dev: true - /@vitest/utils@0.34.4: - resolution: {integrity: sha512-yR2+5CHhp/K4ySY0Qtd+CAL9f5Yh1aXrKfAT42bq6CtlGPh92jIDDDSg7ydlRow1CP+dys4TrOrbELOyNInHSg==} + /@vitest/utils@1.4.0: + resolution: {integrity: sha512-mx3Yd1/6e2Vt/PUC98DcqTirtfxUyAZ32uK82r8rZzbtBeBo+nqgnjx/LvqQdWsrvNtm14VmurNgcf4nqY5gJg==} dependencies: diff-sequences: 29.6.3 - loupe: 2.3.6 - pretty-format: 29.6.3 + estree-walker: 3.0.3 + loupe: 2.3.7 + pretty-format: 29.7.0 dev: true - /acorn-walk@8.2.0: - resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} + /acorn-walk@8.3.2: + resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} engines: {node: '>=0.4.0'} dev: true @@ -1110,7 +1197,7 @@ packages: is-windows: 1.0.2 dev: true - /bob-the-bundler@7.0.1(typescript@5.2.2): + /bob-the-bundler@7.0.1(typescript@5.4.2): resolution: {integrity: sha512-TFpj2AcCzTNVyMZ5ixHqJndJ9KyIUGrgTMMciz88X0HCRDujoUQL+D+61shAY+K20bM4q5Yn/NunbdiPC9drjA==} engines: {node: '>=16', pnpm: '>=8'} hasBin: true @@ -1126,7 +1213,7 @@ packages: p-limit: 4.0.0 resolve.exports: 2.0.2 tslib: 2.6.2 - typescript: 5.2.2 + typescript: 5.4.2 yargs: 17.7.2 zod: 3.22.2 dev: true @@ -1161,10 +1248,6 @@ packages: update-browserslist-db: 1.0.11(browserslist@4.21.10) dev: true - /buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - dev: true - /cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -1195,14 +1278,14 @@ packages: resolution: {integrity: sha512-FbDFnNat3nMnrROzqrsg314zhqN5LGQ1kyyMk2opcrwGbVGpHRhgCWtAgD5YJUqNAiQ+dklreil/c3Qf1dfCTw==} dev: true - /chai@4.3.8: - resolution: {integrity: sha512-vX4YvVVtxlfSZ2VecZgFUTU5qPCYsobVI2O9FmwEXBhDigYGQA6jRXCycIs1yJnnWbZ6/+a2zNIF5DfVCcJBFQ==} + /chai@4.4.1: + resolution: {integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==} engines: {node: '>=4'} dependencies: assertion-error: 1.1.0 - check-error: 1.0.2 + check-error: 1.0.3 deep-eql: 4.1.3 - get-func-name: 2.0.0 + get-func-name: 2.0.2 loupe: 2.3.6 pathval: 1.1.1 type-detect: 4.0.8 @@ -1229,8 +1312,10 @@ packages: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} dev: true - /check-error@1.0.2: - resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} + /check-error@1.0.3: + resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + dependencies: + get-func-name: 2.0.2 dev: true /ci-info@3.8.0: @@ -1553,34 +1638,35 @@ packages: is-symbol: 1.0.4 dev: true - /esbuild@0.18.20: - resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + /esbuild@0.19.12: + resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} engines: {node: '>=12'} hasBin: true requiresBuild: true optionalDependencies: - '@esbuild/android-arm': 0.18.20 - '@esbuild/android-arm64': 0.18.20 - '@esbuild/android-x64': 0.18.20 - '@esbuild/darwin-arm64': 0.18.20 - '@esbuild/darwin-x64': 0.18.20 - '@esbuild/freebsd-arm64': 0.18.20 - '@esbuild/freebsd-x64': 0.18.20 - '@esbuild/linux-arm': 0.18.20 - '@esbuild/linux-arm64': 0.18.20 - '@esbuild/linux-ia32': 0.18.20 - '@esbuild/linux-loong64': 0.18.20 - '@esbuild/linux-mips64el': 0.18.20 - '@esbuild/linux-ppc64': 0.18.20 - '@esbuild/linux-riscv64': 0.18.20 - '@esbuild/linux-s390x': 0.18.20 - '@esbuild/linux-x64': 0.18.20 - '@esbuild/netbsd-x64': 0.18.20 - '@esbuild/openbsd-x64': 0.18.20 - '@esbuild/sunos-x64': 0.18.20 - '@esbuild/win32-arm64': 0.18.20 - '@esbuild/win32-ia32': 0.18.20 - '@esbuild/win32-x64': 0.18.20 + '@esbuild/aix-ppc64': 0.19.12 + '@esbuild/android-arm': 0.19.12 + '@esbuild/android-arm64': 0.19.12 + '@esbuild/android-x64': 0.19.12 + '@esbuild/darwin-arm64': 0.19.12 + '@esbuild/darwin-x64': 0.19.12 + '@esbuild/freebsd-arm64': 0.19.12 + '@esbuild/freebsd-x64': 0.19.12 + '@esbuild/linux-arm': 0.19.12 + '@esbuild/linux-arm64': 0.19.12 + '@esbuild/linux-ia32': 0.19.12 + '@esbuild/linux-loong64': 0.19.12 + '@esbuild/linux-mips64el': 0.19.12 + '@esbuild/linux-ppc64': 0.19.12 + '@esbuild/linux-riscv64': 0.19.12 + '@esbuild/linux-s390x': 0.19.12 + '@esbuild/linux-x64': 0.19.12 + '@esbuild/netbsd-x64': 0.19.12 + '@esbuild/openbsd-x64': 0.19.12 + '@esbuild/sunos-x64': 0.19.12 + '@esbuild/win32-arm64': 0.19.12 + '@esbuild/win32-ia32': 0.19.12 + '@esbuild/win32-x64': 0.19.12 dev: true /escalade@3.1.1: @@ -1599,6 +1685,12 @@ packages: hasBin: true dev: true + /estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + dependencies: + '@types/estree': 1.0.5 + dev: true + /execa@7.1.1: resolution: {integrity: sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q==} engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0} @@ -1614,6 +1706,21 @@ packages: strip-final-newline: 3.0.0 dev: true + /execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + dependencies: + cross-spawn: 7.0.3 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.1.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + dev: true + /extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} dev: true @@ -1638,14 +1745,25 @@ packages: micromatch: 4.0.5 dev: true + /fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: true + /fastq@1.15.0: resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} dependencies: reusify: 1.0.4 dev: true - /fflate@0.8.0: - resolution: {integrity: sha512-FAdS4qMuFjsJj6XHbBaZeXOgaypXp8iw/Tpyuq/w3XA41jjLHT8NPA+n7czH/DDhdncq0nAyDZmPeWXh2qmdIg==} + /fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} dev: true /fill-range@7.0.1: @@ -1678,8 +1796,8 @@ packages: pkg-dir: 4.2.0 dev: true - /flatted@3.2.7: - resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} + /flatted@3.3.1: + resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} dev: true /for-each@0.3.3: @@ -1759,8 +1877,8 @@ packages: engines: {node: 6.* || 8.* || >= 10.*} dev: true - /get-func-name@2.0.0: - resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==} + /get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} dev: true /get-intrinsic@1.2.1: @@ -1777,6 +1895,11 @@ packages: engines: {node: '>=10'} dev: true + /get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + dev: true + /get-symbol-description@1.0.0: resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} engines: {node: '>= 0.4'} @@ -1785,8 +1908,8 @@ packages: get-intrinsic: 1.2.1 dev: true - /get-tsconfig@4.7.0: - resolution: {integrity: sha512-pmjiZ7xtB8URYm74PlGJozDNyhvsVLUcpBa8DZBG3bWHwaHa9bPiRpiSfovw+fjhwONSCWKRyk+JQHEGZmMrzw==} + /get-tsconfig@4.7.3: + resolution: {integrity: sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg==} dependencies: resolve-pkg-maps: 1.0.0 dev: true @@ -1798,13 +1921,13 @@ packages: is-glob: 4.0.3 dev: true - /glob@10.3.4: - resolution: {integrity: sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ==} + /glob@10.3.10: + resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} engines: {node: '>=16 || 14 >=14.17'} hasBin: true dependencies: foreground-child: 3.1.1 - jackspeak: 2.3.3 + jackspeak: 2.3.6 minimatch: 9.0.3 minipass: 7.0.3 path-scurry: 1.10.1 @@ -1859,8 +1982,8 @@ packages: resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} dev: true - /graphql@16.8.0: - resolution: {integrity: sha512-0oKGaR+y3qcS5mCu1vb7KG+a89vjn06C7Ihq/dDl3jA+A8B3TKomvi3CiEcVLJQGalbu8F52LxkOym7U5sSfbg==} + /graphql@16.8.1: + resolution: {integrity: sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} dev: true @@ -1926,6 +2049,11 @@ packages: engines: {node: '>=14.18.0'} dev: true + /human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + dev: true + /iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -2135,8 +2263,8 @@ packages: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true - /jackspeak@2.3.3: - resolution: {integrity: sha512-R2bUw+kVZFS/h1AZqBKrSgDmdmjApzgY0AlCPumopFiAlbUxE2gf+SCuBzQ0cP5hHmUmFYF5yw55T97Th5Kstg==} + /jackspeak@2.3.6: + resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} engines: {node: '>=14'} dependencies: '@isaacs/cliui': 8.0.2 @@ -2153,6 +2281,10 @@ packages: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} dev: true + /js-tokens@8.0.3: + resolution: {integrity: sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==} + dev: true + /js-yaml@3.14.1: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true @@ -2225,9 +2357,12 @@ packages: strip-bom: 3.0.0 dev: true - /local-pkg@0.4.3: - resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==} + /local-pkg@0.5.0: + resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} engines: {node: '>=14'} + dependencies: + mlly: 1.4.2 + pkg-types: 1.0.3 dev: true /locate-path@5.0.0: @@ -2258,8 +2393,15 @@ packages: /loupe@2.3.6: resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==} + deprecated: Please upgrade to 2.3.7 which fixes GHSA-4q6p-r6v2-jvc5 dependencies: - get-func-name: 2.0.0 + get-func-name: 2.0.2 + dev: true + + /loupe@2.3.7: + resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + dependencies: + get-func-name: 2.0.2 dev: true /lower-case@2.0.2: @@ -2293,8 +2435,8 @@ packages: yallist: 4.0.0 dev: true - /magic-string@0.30.3: - resolution: {integrity: sha512-B7xGbll2fG/VjP+SWg4sX3JynwIU0mjoTc6MPpKNuIvftk6u6vqhDnk1R80b8C2GBR6ywqy+1DcKBrevBg+bmw==} + /magic-string@0.30.8: + resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} engines: {node: '>=12'} dependencies: '@jridgewell/sourcemap-codec': 1.4.15 @@ -2393,8 +2535,8 @@ packages: ufo: 1.3.0 dev: true - /mrmime@1.0.1: - resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==} + /mrmime@2.0.0: + resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} engines: {node: '>=10'} dev: true @@ -2405,8 +2547,8 @@ packages: resolution: {integrity: sha512-kMbrH0EObaKmK3nVRKUIIya1dpASHIEusM13S4V1ViHFuxuNxCo+arxoa6j/dbV22YBGjl7UKJm9QQKJ2Crzhg==} dev: true - /nanoid@3.3.6: - resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} + /nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true dev: true @@ -2521,6 +2663,13 @@ packages: yocto-queue: 1.0.0 dev: true + /p-limit@5.0.0: + resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} + engines: {node: '>=18'} + dependencies: + yocto-queue: 1.0.0 + dev: true + /p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} @@ -2624,11 +2773,11 @@ packages: pathe: 1.1.1 dev: true - /postcss@8.4.29: - resolution: {integrity: sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==} + /postcss@8.4.35: + resolution: {integrity: sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==} engines: {node: ^10 || ^12 || >=14} dependencies: - nanoid: 3.3.6 + nanoid: 3.3.7 picocolors: 1.0.0 source-map-js: 1.0.2 dev: true @@ -2643,23 +2792,23 @@ packages: which-pm: 2.0.0 dev: true - /prettier-plugin-pkg@0.18.0(prettier@3.0.3): + /prettier-plugin-pkg@0.18.0(prettier@3.2.5): resolution: {integrity: sha512-cme+OUHj25cVj3HwGK6ek/GkCHhlhM1u/IYspOHYsFImaXMJCmjs8xeCcvLreD0HMX5QxObot+3TtQR3Bd2wHw==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: prettier: ^3.0.0 dependencies: - prettier: 3.0.3 + prettier: 3.2.5 dev: true - /prettier-plugin-sh@0.13.1(prettier@3.0.3): + /prettier-plugin-sh@0.13.1(prettier@3.2.5): resolution: {integrity: sha512-ytMcl1qK4s4BOFGvsc9b0+k9dYECal7U29bL/ke08FEUsF/JLN0j6Peo0wUkFDG4y2UHLMhvpyd6Sd3zDXe/eg==} engines: {node: '>=16.0.0'} peerDependencies: prettier: ^3.0.0 dependencies: mvdan-sh: 0.10.1 - prettier: 3.0.3 + prettier: 3.2.5 sh-syntax: 0.4.1 dev: true @@ -2669,14 +2818,14 @@ packages: hasBin: true dev: true - /prettier@3.0.3: - resolution: {integrity: sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==} + /prettier@3.2.5: + resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==} engines: {node: '>=14'} hasBin: true dev: true - /pretty-format@29.6.3: - resolution: {integrity: sha512-ZsBgjVhFAj5KeK+nHfF1305/By3lechHQSMWCTl8iHSbfOm2TN5nHEtFc/+W7fAyUeCs2n5iow72gld4gW0xDw==} + /pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/schemas': 29.6.3 @@ -2788,19 +2937,34 @@ packages: engines: {iojs: '>=1.0.0', node: '>=0.10.0'} dev: true - /rimraf@5.0.1: - resolution: {integrity: sha512-OfFZdwtd3lZ+XZzYP/6gTACubwFcHdLRqS9UX3UwpU2dnGQYkPFISRwvM3w9IiB2w7bW5qGo/uAwE4SmXXSKvg==} + /rimraf@5.0.5: + resolution: {integrity: sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==} engines: {node: '>=14'} hasBin: true dependencies: - glob: 10.3.4 + glob: 10.3.10 dev: true - /rollup@3.29.1: - resolution: {integrity: sha512-c+ebvQz0VIH4KhhCpDsI+Bik0eT8ZFEVZEYw0cGMVqIP8zc+gnwl7iXCamTw7vzv2MeuZFZfdx5JJIq+ehzDlg==} - engines: {node: '>=14.18.0', npm: '>=8.0.0'} + /rollup@4.13.0: + resolution: {integrity: sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + dependencies: + '@types/estree': 1.0.5 optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.13.0 + '@rollup/rollup-android-arm64': 4.13.0 + '@rollup/rollup-darwin-arm64': 4.13.0 + '@rollup/rollup-darwin-x64': 4.13.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.13.0 + '@rollup/rollup-linux-arm64-gnu': 4.13.0 + '@rollup/rollup-linux-arm64-musl': 4.13.0 + '@rollup/rollup-linux-riscv64-gnu': 4.13.0 + '@rollup/rollup-linux-x64-gnu': 4.13.0 + '@rollup/rollup-linux-x64-musl': 4.13.0 + '@rollup/rollup-win32-arm64-msvc': 4.13.0 + '@rollup/rollup-win32-ia32-msvc': 4.13.0 + '@rollup/rollup-win32-x64-msvc': 4.13.0 fsevents: 2.3.3 dev: true @@ -2915,12 +3079,12 @@ packages: engines: {node: '>=14'} dev: true - /sirv@2.0.3: - resolution: {integrity: sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA==} + /sirv@2.0.4: + resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} engines: {node: '>= 10'} dependencies: - '@polka/url': 1.0.0-next.23 - mrmime: 1.0.1 + '@polka/url': 1.0.0-next.25 + mrmime: 2.0.0 totalist: 3.0.1 dev: true @@ -2952,18 +3116,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /source-map-support@0.5.21: - resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - dev: true - - /source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - dev: true - /spawndamnit@2.0.0: resolution: {integrity: sha512-j4JKEcncSjFlqIwU5L/rp2N5SIPsdxaRsIv678+TZxZ0SRDJTm8JrxJMjE/XuiEZNEir3S8l0Fa3Ke339WI4qA==} dependencies: @@ -3001,8 +3153,8 @@ packages: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} dev: true - /std-env@3.4.3: - resolution: {integrity: sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==} + /std-env@3.7.0: + resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} dev: true /stop-iteration-iterator@1.0.0: @@ -3099,10 +3251,10 @@ packages: min-indent: 1.0.1 dev: true - /strip-literal@1.3.0: - resolution: {integrity: sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==} + /strip-literal@2.0.0: + resolution: {integrity: sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==} dependencies: - acorn: 8.10.0 + js-tokens: 8.0.3 dev: true /supports-color@5.5.0: @@ -3129,17 +3281,17 @@ packages: engines: {node: '>=8'} dev: true - /tinybench@2.5.0: - resolution: {integrity: sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==} + /tinybench@2.6.0: + resolution: {integrity: sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==} dev: true - /tinypool@0.7.0: - resolution: {integrity: sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==} + /tinypool@0.8.2: + resolution: {integrity: sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==} engines: {node: '>=14.0.0'} dev: true - /tinyspy@2.1.1: - resolution: {integrity: sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w==} + /tinyspy@2.2.1: + resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} engines: {node: '>=14.0.0'} dev: true @@ -3184,13 +3336,13 @@ packages: /tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - /tsx@3.12.8: - resolution: {integrity: sha512-Lt9KYaRGF023tlLInPj8rgHwsZU8qWLBj4iRXNWxTfjIkU7canGL806AqKear1j722plHuiYNcL2ZCo6uS9UJA==} + /tsx@4.7.1: + resolution: {integrity: sha512-8d6VuibXHtlN5E3zFkgY8u4DX7Y3Z27zvvPKVmLon/D4AjuKzarkUBTLDBgj9iTQ0hg5xM7c/mYiRVM+HETf0g==} + engines: {node: '>=18.0.0'} hasBin: true dependencies: - '@esbuild-kit/cjs-loader': 2.4.2 - '@esbuild-kit/core-utils': 3.2.3 - '@esbuild-kit/esm-loader': 2.5.5 + esbuild: 0.19.12 + get-tsconfig: 4.7.3 optionalDependencies: fsevents: 2.3.3 dev: true @@ -3267,8 +3419,8 @@ packages: is-typed-array: 1.1.12 dev: true - /typescript@5.2.2: - resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} + /typescript@5.4.2: + resolution: {integrity: sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==} engines: {node: '>=14.17'} hasBin: true dev: true @@ -3325,17 +3477,16 @@ packages: spdx-expression-parse: 3.0.1 dev: true - /vite-node@0.34.4(@types/node@20.6.0): - resolution: {integrity: sha512-ho8HtiLc+nsmbwZMw8SlghESEE3KxJNp04F/jPUCLVvaURwt0d+r9LxEqCX5hvrrOQ0GSyxbYr5ZfRYhQ0yVKQ==} - engines: {node: '>=v14.18.0'} + /vite-node@1.4.0(@types/node@20.6.0): + resolution: {integrity: sha512-VZDAseqjrHgNd4Kh8icYHWzTKSCZMhia7GyHfhtzLW33fZlG9SwsB6CEhgyVOWkJfJ2pFLrp/Gj1FSfAiqH9Lw==} + engines: {node: ^18.0.0 || >=20.0.0} hasBin: true dependencies: cac: 6.7.14 debug: 4.3.4 - mlly: 1.4.2 pathe: 1.1.1 picocolors: 1.0.0 - vite: 4.4.9(@types/node@20.6.0) + vite: 5.1.6(@types/node@20.6.0) transitivePeerDependencies: - '@types/node' - less @@ -3347,12 +3498,12 @@ packages: - terser dev: true - /vite@4.4.9(@types/node@20.6.0): - resolution: {integrity: sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==} - engines: {node: ^14.18.0 || >=16.0.0} + /vite@5.1.6(@types/node@20.6.0): + resolution: {integrity: sha512-yYIAZs9nVfRJ/AiOLCA91zzhjsHUgMjB+EigzFb6W2XTLO8JixBCKCjvhKZaye+NKYHCrkv3Oh50dH9EdLU2RA==} + engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: - '@types/node': '>= 14' + '@types/node': ^18.0.0 || >=20.0.0 less: '*' lightningcss: ^1.21.0 sass: '*' @@ -3376,29 +3527,29 @@ packages: optional: true dependencies: '@types/node': 20.6.0 - esbuild: 0.18.20 - postcss: 8.4.29 - rollup: 3.29.1 + esbuild: 0.19.12 + postcss: 8.4.35 + rollup: 4.13.0 optionalDependencies: fsevents: 2.3.3 dev: true - /vitest@0.34.4(@vitest/ui@0.34.4): - resolution: {integrity: sha512-SE/laOsB6995QlbSE6BtkpXDeVNLJc1u2LHRG/OpnN4RsRzM3GQm4nm3PQCK5OBtrsUqnhzLdnT7se3aeNGdlw==} - engines: {node: '>=v14.18.0'} + /vitest@1.4.0(@types/node@20.6.0)(@vitest/ui@1.4.0): + resolution: {integrity: sha512-gujzn0g7fmwf83/WzrDTnncZt2UiXP41mHuFYFrdwaLRVQ6JYQEiME2IfEjU3vcFL3VKa75XhI3lFgn+hfVsQw==} + engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' - '@vitest/browser': '*' - '@vitest/ui': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 1.4.0 + '@vitest/ui': 1.4.0 happy-dom: '*' jsdom: '*' - playwright: '*' - safaridriver: '*' - webdriverio: '*' peerDependenciesMeta: '@edge-runtime/vm': optional: true + '@types/node': + optional: true '@vitest/browser': optional: true '@vitest/ui': @@ -3407,37 +3558,28 @@ packages: optional: true jsdom: optional: true - playwright: - optional: true - safaridriver: - optional: true - webdriverio: - optional: true dependencies: - '@types/chai': 4.3.6 - '@types/chai-subset': 1.3.3 '@types/node': 20.6.0 - '@vitest/expect': 0.34.4 - '@vitest/runner': 0.34.4 - '@vitest/snapshot': 0.34.4 - '@vitest/spy': 0.34.4 - '@vitest/ui': 0.34.4(vitest@0.34.4) - '@vitest/utils': 0.34.4 - acorn: 8.10.0 - acorn-walk: 8.2.0 - cac: 6.7.14 - chai: 4.3.8 + '@vitest/expect': 1.4.0 + '@vitest/runner': 1.4.0 + '@vitest/snapshot': 1.4.0 + '@vitest/spy': 1.4.0 + '@vitest/ui': 1.4.0(vitest@1.4.0) + '@vitest/utils': 1.4.0 + acorn-walk: 8.3.2 + chai: 4.4.1 debug: 4.3.4 - local-pkg: 0.4.3 - magic-string: 0.30.3 + execa: 8.0.1 + local-pkg: 0.5.0 + magic-string: 0.30.8 pathe: 1.1.1 picocolors: 1.0.0 - std-env: 3.4.3 - strip-literal: 1.3.0 - tinybench: 2.5.0 - tinypool: 0.7.0 - vite: 4.4.9(@types/node@20.6.0) - vite-node: 0.34.4(@types/node@20.6.0) + std-env: 3.7.0 + strip-literal: 2.0.0 + tinybench: 2.6.0 + tinypool: 0.8.2 + vite: 5.1.6(@types/node@20.6.0) + vite-node: 1.4.0(@types/node@20.6.0) why-is-node-running: 2.2.2 transitivePeerDependencies: - less diff --git a/src/subgraph/helpers.ts b/src/subgraph/helpers.ts index 741eb0e..84b32b5 100644 --- a/src/subgraph/helpers.ts +++ b/src/subgraph/helpers.ts @@ -217,7 +217,7 @@ export function visitFields({ interceptUnknownField, interceptInterfaceType, }); - break; + continue; } const selectionFieldDef: FieldDefinitionNode | undefined = diff --git a/src/subgraph/state.ts b/src/subgraph/state.ts index 5abb035..0813c16 100644 --- a/src/subgraph/state.ts +++ b/src/subgraph/state.ts @@ -6,6 +6,8 @@ import { EnumTypeDefinitionNode, EnumTypeExtensionNode, Kind, + ObjectTypeDefinitionNode, + ObjectTypeExtensionNode, OperationTypeNode, SchemaDefinitionNode, specifiedDirectives as specifiedDirectiveTypes, @@ -14,11 +16,7 @@ import { } from 'graphql'; import { print } from '../graphql/printer.js'; import { TypeNodeInfo } from '../graphql/type-node-info.js'; -import { - FederationImports, - FederationVersion, - isFederationLink, -} from '../specifications/federation.js'; +import { FederationVersion, isFederationLink } from '../specifications/federation.js'; import { Link } from '../specifications/link.js'; import { printOutputType } from './helpers.js'; @@ -92,13 +90,13 @@ export interface ObjectType { ast: { directives: DirectiveNode[]; }; - interfaceObjectTypeName?: string; // added for @interfaceObject (trkohler) } export interface InterfaceType { kind: TypeKind.INTERFACE; name: string; fields: Map; + fieldsUsedAsKeys: Set; extension: boolean; keys: Key[]; inaccessible: boolean; @@ -108,9 +106,9 @@ export interface InterfaceType { tags: Set; interfaces: Set; isDefinition: boolean; + isInterfaceObject: boolean; description?: Description; implementedBy: Set; - interfaceObjects: Map; // added for @interfaceObject (trkohler) ast: { directives: DirectiveNode[]; }; @@ -157,6 +155,7 @@ export interface EnumType { export interface Field { name: string; type: string; + isLeaf: boolean; args: Map; external: boolean; inaccessible: boolean; @@ -170,6 +169,7 @@ export interface Field { required: boolean; provided: boolean; shareable: boolean; + usedAsKey: boolean; used: boolean; tags: Set; description?: Description; @@ -230,8 +230,6 @@ export interface SubgraphState { id: string; name: string; version: FederationVersion; - // added imports to check for @interfaceObject (trkohler) - imports: FederationImports; url?: string; }; /** @@ -268,7 +266,6 @@ export function createSubgraphStateBuilder( graph: { id: string; name: string; url?: string }, typeDefs: DocumentNode, version: FederationVersion, - imports: FederationImports, links: readonly Link[], ) { const linksWithDirective = links.filter( @@ -287,7 +284,6 @@ export function createSubgraphStateBuilder( graph: { ...graph, version, - imports, }, types: new Map(), schema: {}, @@ -305,6 +301,19 @@ export function createSubgraphStateBuilder( const schemaDef = typeDefs.definitions.find(isSchemaDefinition); + const leafTypeNames = new Set(specifiedScalars); + + for (const typeDef of typeDefs.definitions) { + if ( + typeDef.kind === Kind.SCALAR_TYPE_DEFINITION || + typeDef.kind === Kind.SCALAR_TYPE_EXTENSION || + typeDef.kind === Kind.ENUM_TYPE_DEFINITION || + typeDef.kind === Kind.ENUM_TYPE_EXTENSION + ) { + leafTypeNames.add(typeDef.name.value); + } + } + const expectedQueryTypeName = decideOnRootTypeName(schemaDef, OperationTypeNode.QUERY, 'Query'); const expectedMutationTypeName = decideOnRootTypeName( schemaDef, @@ -324,8 +333,13 @@ export function createSubgraphStateBuilder( const directiveBuilder = directiveFactory(state); const scalarTypeBuilder = scalarTypeFactory(state); - const objectTypeBuilder = objectTypeFactory(state, renameObjectType); const interfaceTypeBuilder = interfaceTypeFactory(state); + const objectTypeBuilder = objectTypeFactory( + state, + renameObjectType, + interfaceTypeBuilder, + isInterfaceObject, + ); const inputObjectTypeBuilder = inputObjectTypeFactory(state); const unionTypeBuilder = unionTypeFactory(state); const enumTypeBuilder = enumTypeFactory(state); @@ -346,7 +360,22 @@ export function createSubgraphStateBuilder( return typeName; } + function isInterfaceObject(typeName: string) { + const found = state.types.get(typeName); + + if (!found) { + return false; + } + + if (found.kind !== TypeKind.INTERFACE) { + return false; + } + + return found.isInterfaceObject; + } + return { + isInterfaceObject, directive: directiveBuilder, scalarType: scalarTypeBuilder, objectType: objectTypeBuilder, @@ -381,6 +410,7 @@ export function createSubgraphStateBuilder( typeDef.kind === Kind.OBJECT_TYPE_EXTENSION; const outputTypeName = resolveTypeName(node.type); + const isLeaf = leafTypeNames.has(outputTypeName); const referencesEnumType = enumTypesByName.has(outputTypeName); if (referencesEnumType) { @@ -390,12 +420,17 @@ export function createSubgraphStateBuilder( ); } - if (isInterfaceType) { + if (isInterfaceType || isInterfaceObject(typeDef.name.value)) { interfaceTypeBuilder.field.setType( typeDef.name.value, node.name.value, printOutputType(node.type), ); + interfaceTypeBuilder.field.setUsed(typeDef.name.value, node.name.value); + + if (isLeaf) { + interfaceTypeBuilder.field.setLeaf(typeDef.name.value, node.name.value); + } return; } @@ -410,6 +445,10 @@ export function createSubgraphStateBuilder( printOutputType(node.type), ); + if (isLeaf) { + objectTypeBuilder.field.setLeaf(typeDef.name.value, node.name.value); + } + if (typeDef.kind === Kind.OBJECT_TYPE_EXTENSION /* TODO: || has @extends */) { objectTypeBuilder.field.setExtension(typeDef.name.value, node.name.value); } @@ -517,6 +556,18 @@ export function createSubgraphStateBuilder( } }, ObjectTypeDefinition(node) { + if (hasInterfaceObjectDirective(node)) { + interfaceTypeBuilder.setDefinition(node.name.value); + interfaceTypeBuilder.setInterfaceObject(node.name.value); + + if (node.interfaces) { + for (const interfaceNode of node.interfaces) { + interfaceTypeBuilder.setInterface(node.name.value, interfaceNode.name.value); + } + } + return; + } + objectTypeBuilder.setDefinition(node.name.value); // Set query, mutation or subscription types as found in the schema @@ -535,6 +586,9 @@ export function createSubgraphStateBuilder( } }, ObjectTypeExtension(node) { + if (hasInterfaceObjectDirective(node)) { + } + // Set query, mutation or subscription types as found in the schema if (node.name.value === expectedQueryTypeName) { state.schema.queryType = renameObjectType(node.name.value); @@ -637,9 +691,6 @@ export function createSubgraphStateBuilder( ); } else { objectTypeBuilder.setDirective(typeDef.name.value, node); - if (node.name.value == 'interfaceObject') { - objectTypeBuilder.setInterfaceObject(typeDef.name.value); - } } break; case Kind.INTERFACE_TYPE_DEFINITION: @@ -778,11 +829,6 @@ export function createSubgraphStateBuilder( break; } } - } else if (node.name.value == 'interfaceObject') { - const typeDef = typeNodeInfo.getTypeDef(); - if (typeDef && typeDef.kind === Kind.OBJECT_TYPE_DEFINITION) { - objectTypeBuilder.setInterfaceObject(typeDef.name.value); - } } }, DirectiveDefinition(node) { @@ -1022,12 +1068,25 @@ function scalarTypeFactory(state: SubgraphState) { }; } -function objectTypeFactory(state: SubgraphState, renameObject: (typeName: string) => string) { +function objectTypeFactory( + state: SubgraphState, + renameObject: (typeName: string) => string, + interfaceTypeBuilder: ReturnType, + isInterfaceObject: (typeName: string) => boolean, +) { return { setDefinition(typeName: string) { + if (isInterfaceObject(typeName)) { + return interfaceTypeBuilder.setDefinition(typeName); + } + getOrCreateObjectType(state, renameObject, typeName).isDefinition = true; }, setExtension(typeName: string, extensionType: '@extends' | 'extend') { + if (isInterfaceObject(typeName)) { + return interfaceTypeBuilder.setExtension(typeName); + } + const objectType = getOrCreateObjectType(state, renameObject, typeName); objectType.extension = true; if (objectType.extensionType !== '@extends') { @@ -1035,9 +1094,16 @@ function objectTypeFactory(state: SubgraphState, renameObject: (typeName: string } }, setDescription(typeName: string, description: Description) { + if (isInterfaceObject(typeName)) { + return interfaceTypeBuilder.setDescription(typeName, description); + } getOrCreateObjectType(state, renameObject, typeName).description = description; }, setExternal(typeName: string) { + if (isInterfaceObject(typeName)) { + return; + } + const objectType = getOrCreateObjectType(state, renameObject, typeName); objectType.external = true; @@ -1046,10 +1112,17 @@ function objectTypeFactory(state: SubgraphState, renameObject: (typeName: string } }, setInterface(typeName: string, interfaceName: string) { + if (isInterfaceObject(typeName)) { + return interfaceTypeBuilder.setInterface(typeName, interfaceName); + } + getOrCreateObjectType(state, renameObject, typeName).interfaces.add(interfaceName); getOrCreateInterfaceType(state, interfaceName).implementedBy.add(typeName); }, setKey(typeName: string, fields: string, fieldsUsedInKey: Set, resolvable: boolean) { + if (isInterfaceObject(typeName)) { + return interfaceTypeBuilder.setKey(typeName, fields, fieldsUsedInKey, resolvable); + } const objectType = getOrCreateObjectType(state, renameObject, typeName); objectType.keys.push({ fields, resolvable }); @@ -1058,6 +1131,10 @@ function objectTypeFactory(state: SubgraphState, renameObject: (typeName: string } }, setInaccessible(typeName: string) { + if (isInterfaceObject(typeName)) { + return interfaceTypeBuilder.setInaccessible(typeName); + } + const objectType = getOrCreateObjectType(state, renameObject, typeName); objectType.inaccessible = true; @@ -1066,86 +1143,193 @@ function objectTypeFactory(state: SubgraphState, renameObject: (typeName: string // } }, setAuthenticated(typeName: string) { + if (isInterfaceObject(typeName)) { + return interfaceTypeBuilder.setAuthenticated(typeName); + } + const objectType = getOrCreateObjectType(state, renameObject, typeName); objectType.authenticated = true; }, setPolicies(typeName: string, policies: string[][]) { + if (isInterfaceObject(typeName)) { + return interfaceTypeBuilder.setPolicies(typeName, policies); + } + getOrCreateObjectType(state, renameObject, typeName).policies.push(...policies); }, setScopes(typeName: string, scopes: string[][]) { + if (isInterfaceObject(typeName)) { + return interfaceTypeBuilder.setScopes(typeName, scopes); + } + getOrCreateObjectType(state, renameObject, typeName).scopes.push(...scopes); }, setShareable(typeName: string) { + if (isInterfaceObject(typeName)) { + return; + } + getOrCreateObjectType(state, renameObject, typeName).shareable = true; }, setTag(typeName: string, tag: string) { + if (isInterfaceObject(typeName)) { + return interfaceTypeBuilder.setTag(typeName, tag); + } + getOrCreateObjectType(state, renameObject, typeName).tags.add(tag); }, setDirective(typeName: string, directive: DirectiveNode) { + if (isInterfaceObject(typeName)) { + return interfaceTypeBuilder.setDirective(typeName, directive); + } + getOrCreateObjectType(state, renameObject, typeName).ast.directives.push(directive); }, field: { setType(typeName: string, fieldName: string, fieldType: string) { + if (isInterfaceObject(typeName)) { + return interfaceTypeBuilder.field.setType(typeName, fieldName, fieldType); + } + getOrCreateObjectField(state, renameObject, typeName, fieldName).type = fieldType; }, + setLeaf(typeName: string, fieldName: string) { + if (isInterfaceObject(typeName)) { + return interfaceTypeBuilder.field.setLeaf(typeName, fieldName); + } + + getOrCreateObjectField(state, renameObject, typeName, fieldName).isLeaf = true; + }, setExtension(typeName: string, fieldName: string) { + if (isInterfaceObject(typeName)) { + return; + } + getOrCreateObjectField(state, renameObject, typeName, fieldName).extension = true; }, setDirective(typeName: string, fieldName: string, directive: DirectiveNode) { + if (isInterfaceObject(typeName)) { + return interfaceTypeBuilder.field.setDirective(typeName, fieldName, directive); + } + getOrCreateObjectField(state, renameObject, typeName, fieldName).ast.directives.push( directive, ); }, setDescription(typeName: string, fieldName: string, description: Description) { + if (isInterfaceObject(typeName)) { + return interfaceTypeBuilder.field.setDescription(typeName, fieldName, description); + } + getOrCreateObjectField(state, renameObject, typeName, fieldName).description = description; }, setDeprecated(typeName: string, fieldName: string, reason?: string) { + if (isInterfaceObject(typeName)) { + return interfaceTypeBuilder.field.setDeprecated(typeName, fieldName, reason); + } + getOrCreateObjectField(state, renameObject, typeName, fieldName).deprecated = { reason, deprecated: true, }; }, setAuthenticated(typeName: string, fieldName: string) { + if (isInterfaceObject(typeName)) { + return interfaceTypeBuilder.field.setAuthenticated(typeName, fieldName); + } + getOrCreateObjectField(state, renameObject, typeName, fieldName).authenticated = true; }, setPolicies(typeName: string, fieldName: string, policies: string[][]) { + if (isInterfaceObject(typeName)) { + return interfaceTypeBuilder.field.setPolicies(typeName, fieldName, policies); + } + getOrCreateObjectField(state, renameObject, typeName, fieldName).policies.push(...policies); }, setScopes(typeName: string, fieldName: string, scopes: string[][]) { + if (isInterfaceObject(typeName)) { + return interfaceTypeBuilder.field.setScopes(typeName, fieldName, scopes); + } + getOrCreateObjectField(state, renameObject, typeName, fieldName).scopes.push(...scopes); }, setExternal(typeName: string, fieldName: string) { + if (isInterfaceObject(typeName)) { + return interfaceTypeBuilder.field.setExternal(typeName, fieldName); + } + getOrCreateObjectField(state, renameObject, typeName, fieldName).external = true; }, setInaccessible(typeName: string, fieldName: string) { + if (isInterfaceObject(typeName)) { + return interfaceTypeBuilder.field.setInaccessible(typeName, fieldName); + } + getOrCreateObjectField(state, renameObject, typeName, fieldName).inaccessible = true; }, setOverride(typeName: string, fieldName: string, override: string) { + if (isInterfaceObject(typeName)) { + return interfaceTypeBuilder.field.setOverride(typeName, fieldName, override); + } + getOrCreateObjectField(state, renameObject, typeName, fieldName).override = override; }, setProvides(typeName: string, fieldName: string, provides: string) { + if (isInterfaceObject(typeName)) { + return; + } + getOrCreateObjectField(state, renameObject, typeName, fieldName).provides = provides; }, setRequires(typeName: string, fieldName: string, requires: string) { + if (isInterfaceObject(typeName)) { + return interfaceTypeBuilder.field.setRequires(typeName, fieldName, requires); + } + getOrCreateObjectField(state, renameObject, typeName, fieldName).requires = requires; }, markAsProvided(typeName: string, fieldName: string) { + if (isInterfaceObject(typeName)) { + return; + } + getOrCreateObjectField(state, renameObject, typeName, fieldName).provided = true; }, markedAsRequired(typeName: string, fieldName: string) { + if (isInterfaceObject(typeName)) { + return; + } + getOrCreateObjectField(state, renameObject, typeName, fieldName).required = true; }, setShareable(typeName: string, fieldName: string) { + if (isInterfaceObject(typeName)) { + return interfaceTypeBuilder.field.setShareable(typeName, fieldName); + } + getOrCreateObjectField(state, renameObject, typeName, fieldName).shareable = true; }, setTag(typeName: string, fieldName: string, tag: string) { + if (isInterfaceObject(typeName)) { + return interfaceTypeBuilder.field.setTag(typeName, fieldName, tag); + } + getOrCreateObjectField(state, renameObject, typeName, fieldName).tags.add(tag); }, setUsed(typeName: string, fieldName: string) { + if (isInterfaceObject(typeName)) { + return interfaceTypeBuilder.field.setUsed(typeName, fieldName); + } + getOrCreateObjectField(state, renameObject, typeName, fieldName).used = true; }, arg: { setType(typeName: string, fieldName: string, argName: string, argType: string) { + if (isInterfaceObject(typeName)) { + return interfaceTypeBuilder.field.arg.setType(typeName, fieldName, argName, argType); + } + getOrCreateObjectFieldArgument(state, renameObject, typeName, fieldName, argName).type = argType; }, @@ -1155,6 +1339,15 @@ function objectTypeFactory(state: SubgraphState, renameObject: (typeName: string argName: string, description: Description, ) { + if (isInterfaceObject(typeName)) { + return interfaceTypeBuilder.field.arg.setDescription( + typeName, + fieldName, + argName, + description, + ); + } + getOrCreateObjectFieldArgument( state, renameObject, @@ -1164,6 +1357,15 @@ function objectTypeFactory(state: SubgraphState, renameObject: (typeName: string ).description = description; }, setDeprecated(typeName: string, fieldName: string, argName: string, reason?: string) { + if (isInterfaceObject(typeName)) { + return interfaceTypeBuilder.field.arg.setDeprecated( + typeName, + fieldName, + argName, + reason, + ); + } + getOrCreateObjectFieldArgument( state, renameObject, @@ -1181,6 +1383,15 @@ function objectTypeFactory(state: SubgraphState, renameObject: (typeName: string argName: string, directive: DirectiveNode, ) { + if (isInterfaceObject(typeName)) { + return interfaceTypeBuilder.field.arg.setDirective( + typeName, + fieldName, + argName, + directive, + ); + } + getOrCreateObjectFieldArgument( state, renameObject, @@ -1195,6 +1406,15 @@ function objectTypeFactory(state: SubgraphState, renameObject: (typeName: string argName: string, defaultValue: string, ) { + if (isInterfaceObject(typeName)) { + return interfaceTypeBuilder.field.arg.setDefaultValue( + typeName, + fieldName, + argName, + defaultValue, + ); + } + getOrCreateObjectFieldArgument( state, renameObject, @@ -1204,6 +1424,10 @@ function objectTypeFactory(state: SubgraphState, renameObject: (typeName: string ).defaultValue = defaultValue; }, setInaccessible(typeName: string, fieldName: string, argName: string) { + if (isInterfaceObject(typeName)) { + return interfaceTypeBuilder.field.arg.setInaccessible(typeName, fieldName, argName); + } + getOrCreateObjectFieldArgument( state, renameObject, @@ -1213,6 +1437,10 @@ function objectTypeFactory(state: SubgraphState, renameObject: (typeName: string ).inaccessible = true; }, setTag(typeName: string, fieldName: string, argName: string, tag: string) { + if (isInterfaceObject(typeName)) { + return interfaceTypeBuilder.field.arg.setTag(typeName, fieldName, argName, tag); + } + getOrCreateObjectFieldArgument( state, renameObject, @@ -1223,10 +1451,6 @@ function objectTypeFactory(state: SubgraphState, renameObject: (typeName: string }, }, }, - setInterfaceObject(typeName: string) { - const type = getOrCreateObjectType(state, renameObject, typeName); - type.interfaceObjectTypeName = typeName; - }, }; } @@ -1248,8 +1472,16 @@ function interfaceTypeFactory(state: SubgraphState) { setInterface(typeName: string, interfaceName: string) { getOrCreateInterfaceType(state, typeName).interfaces.add(interfaceName); }, + setInterfaceObject(typeName: string) { + getOrCreateInterfaceType(state, typeName).isInterfaceObject = true; + }, setKey(typeName: string, fields: string, fieldsUsedInKey: Set, resolvable: boolean) { - getOrCreateInterfaceType(state, typeName).keys.push({ fields, resolvable }); + const interfaceType = getOrCreateInterfaceType(state, typeName); + interfaceType.keys.push({ fields, resolvable }); + + for (const field of fieldsUsedInKey) { + interfaceType.fieldsUsedAsKeys.add(field); + } }, setInaccessible(typeName: string) { const objectType = getOrCreateInterfaceType(state, typeName); @@ -1282,6 +1514,10 @@ function interfaceTypeFactory(state: SubgraphState) { setType(typeName: string, fieldName: string, fieldType: string) { getOrCreateInterfaceField(state, typeName, fieldName).type = fieldType; }, + setLeaf(typeName: string, fieldName: string) { + getOrCreateInterfaceField(state, typeName, fieldName).isLeaf = true; + }, + setExternal(typeName: string, fieldName: string) { getOrCreateInterfaceField(state, typeName, fieldName).external = true; }, @@ -1637,6 +1873,7 @@ function getOrCreateInterfaceType(state: SubgraphState, typeName: string): Inter kind: TypeKind.INTERFACE, name: typeName, fields: new Map(), + fieldsUsedAsKeys: new Set(), extension: false, keys: [], inaccessible: false, @@ -1647,7 +1884,7 @@ function getOrCreateInterfaceType(state: SubgraphState, typeName: string): Inter interfaces: new Set(), implementedBy: new Set(), isDefinition: false, - interfaceObjects: new Map(), + isInterfaceObject: false, ast: { directives: [], }, @@ -1758,6 +1995,8 @@ function getOrCreateObjectField( const field: Field = { name: fieldName, type: MISSING, + usedAsKey: false, + isLeaf: false, external: false, inaccessible: false, authenticated: false, @@ -1799,6 +2038,8 @@ function getOrCreateInterfaceField( const field: Field = { name: fieldName, + usedAsKey: false, + isLeaf: false, type: MISSING, external: false, inaccessible: false, @@ -1964,3 +2205,7 @@ function decideOnRootTypeName( .value ?? defaultName ); } + +function hasInterfaceObjectDirective(node: ObjectTypeDefinitionNode | ObjectTypeExtensionNode) { + return node.directives?.some(d => d.name.value === 'interfaceObject'); +} diff --git a/src/subgraph/validation/rules/elements/interface-object.ts b/src/subgraph/validation/rules/elements/interface-object.ts index 62ea1af..7a03d37 100644 --- a/src/subgraph/validation/rules/elements/interface-object.ts +++ b/src/subgraph/validation/rules/elements/interface-object.ts @@ -1,14 +1,6 @@ -import { ASTVisitor } from 'graphql'; +import { ASTVisitor, GraphQLError, Kind } from 'graphql'; import { validateDirectiveAgainstOriginal } from '../../../helpers.js'; import type { SubgraphValidationContext } from '../../validation-context.js'; -import { FederationImports } from "../../../../specifications/federation.js"; - -// new utilities for @interfaceObject (trkohler) -export const allowedInterfaceObjectVersion = ["v2.3", "v2.4", "v2.5", "v2.6"] -export const importsAllowInterfaceObject = (imports: FederationImports) => { - const allowed = imports.some((importItem) => importItem.name == "@interfaceObject") && imports.some((importItem) => importItem.name == "@key") - return allowed -} export function InterfaceObjectRules(context: SubgraphValidationContext): ASTVisitor { return { @@ -20,10 +12,42 @@ export function InterfaceObjectRules(context: SubgraphValidationContext): ASTVis return; } - if (!context.satisfiesVersionRange('> v2.3')) { + if (context.satisfiesVersionRange('< v2.3')) { + context.reportError( + new GraphQLError( + `@interfaceObject is not yet supported. See https://github.com/the-guild-org/federation/issues/7`, + { + extensions: { code: 'UNSUPPORTED_FEATURE' }, + }, + ), + ); + return; + } + + const typeDef = context.typeNodeInfo.getTypeDef(); + + if (!typeDef) { return; } + if ( + typeDef.kind !== Kind.OBJECT_TYPE_DEFINITION && + typeDef.kind !== Kind.OBJECT_TYPE_EXTENSION + ) { + // handled by directive location validation + return; + } + + if (!typeDef.directives?.some(d => d.name.value === 'key')) { + context.reportError( + new GraphQLError( + `The @interfaceObject directive can only be applied to entity types but type "${typeDef.name.value}" has no @key in this subgraph.`, + { + extensions: { code: 'INTERFACE_OBJECT_USAGE_ERROR' }, + }, + ), + ); + } }, }; } diff --git a/src/subgraph/validation/rules/elements/key.ts b/src/subgraph/validation/rules/elements/key.ts index 9ad5f31..8d29fb3 100644 --- a/src/subgraph/validation/rules/elements/key.ts +++ b/src/subgraph/validation/rules/elements/key.ts @@ -35,11 +35,12 @@ export function KeyRules(context: SubgraphValidationContext): ASTVisitor { const typeCoordinate = typeDef.name.value; - const usedOnInterface = - typeDef.kind === Kind.INTERFACE_TYPE_DEFINITION || - typeDef.kind === Kind.INTERFACE_TYPE_EXTENSION; const usedOnObject = typeDef.kind === Kind.OBJECT_TYPE_DEFINITION || typeDef.kind === Kind.OBJECT_TYPE_EXTENSION; + const usedOnInterface = + typeDef.kind === Kind.INTERFACE_TYPE_DEFINITION || + typeDef.kind === Kind.INTERFACE_TYPE_EXTENSION || + (usedOnObject && context.stateBuilder.isInterfaceObject(typeDef.name.value)); if (!usedOnObject && !usedOnInterface) { return; // Let regular validation handle this diff --git a/src/subgraph/validation/rules/elements/policy.ts b/src/subgraph/validation/rules/elements/policy.ts index 7bd084d..1a669e3 100644 --- a/src/subgraph/validation/rules/elements/policy.ts +++ b/src/subgraph/validation/rules/elements/policy.ts @@ -76,8 +76,9 @@ export function PolicyRule(context: SubgraphValidationContext): ASTVisitor { } if ( - typeDef.kind === Kind.OBJECT_TYPE_DEFINITION || - typeDef.kind === Kind.OBJECT_TYPE_EXTENSION + (typeDef.kind === Kind.OBJECT_TYPE_DEFINITION || + typeDef.kind === Kind.OBJECT_TYPE_EXTENSION) && + !context.stateBuilder.isInterfaceObject(typeDef.name.value) ) { context.stateBuilder.objectType.field.setPolicies( typeDef.name.value, diff --git a/src/subgraph/validation/rules/elements/shareable.ts b/src/subgraph/validation/rules/elements/shareable.ts index a304bd5..f898f32 100644 --- a/src/subgraph/validation/rules/elements/shareable.ts +++ b/src/subgraph/validation/rules/elements/shareable.ts @@ -20,8 +20,9 @@ export function ShareableRules(context: SubgraphValidationContext): ASTVisitor { } if ( - typeDef.kind === Kind.OBJECT_TYPE_DEFINITION || - typeDef.kind === Kind.OBJECT_TYPE_EXTENSION + (typeDef.kind === Kind.OBJECT_TYPE_DEFINITION || + typeDef.kind === Kind.OBJECT_TYPE_EXTENSION) && + !context.stateBuilder.isInterfaceObject(typeDef.name.value) ) { if (fieldDef) { context.stateBuilder.objectType.field.setShareable( diff --git a/src/subgraph/validation/rules/elements/tag.ts b/src/subgraph/validation/rules/elements/tag.ts index 5cd1921..f45e030 100644 --- a/src/subgraph/validation/rules/elements/tag.ts +++ b/src/subgraph/validation/rules/elements/tag.ts @@ -61,7 +61,8 @@ export function TagRules(context: SubgraphValidationContext): ASTVisitor { if ( typeDef.kind === Kind.INTERFACE_TYPE_DEFINITION || - typeDef.kind === Kind.INTERFACE_TYPE_EXTENSION + typeDef.kind === Kind.INTERFACE_TYPE_EXTENSION || + context.stateBuilder.isInterfaceObject(typeDef.name.value) ) { context.stateBuilder.interfaceType.field.setTag( typeDef.name.value, diff --git a/src/subgraph/validation/rules/known-federation-directive-rule.ts b/src/subgraph/validation/rules/known-federation-directive-rule.ts index 34984cb..315b3df 100644 --- a/src/subgraph/validation/rules/known-federation-directive-rule.ts +++ b/src/subgraph/validation/rules/known-federation-directive-rule.ts @@ -26,6 +26,16 @@ export function KnownFederationDirectivesRule(context: SubgraphValidationContext Directive(node) { const name = node.name.value; + if (!availableDirectivesSet.has(name) && name === 'interfaceObject') { + context.reportError( + new GraphQLError( + `Unknown directive "@interfaceObject". If you meant the "@interfaceObject" federation 2 directive, note that this schema is a federation 1 schema. To be a federation 2 schema, it needs to @link to the federation specification v2.`, + { nodes: node, extensions: { code: 'INVALID_GRAPHQL' } }, + ), + ); + return; + } + if ( !availableDirectivesSet.has(name) && knownDirectivesSet.has(name) && diff --git a/src/subgraph/validation/rules/only-interface-implementation-rule.ts b/src/subgraph/validation/rules/only-interface-implementation-rule.ts new file mode 100644 index 0000000..4c02850 --- /dev/null +++ b/src/subgraph/validation/rules/only-interface-implementation-rule.ts @@ -0,0 +1,88 @@ +import { + ASTVisitor, + GraphQLError, + Kind, + ObjectTypeDefinitionNode, + ObjectTypeExtensionNode, +} from 'graphql'; +import type { SimpleValidationContext } from '../validation-context.js'; + +export function OnlyInterfaceImplementationRule(context: SimpleValidationContext): ASTVisitor { + const { definitions } = context.getDocument(); + let filled = false; + const typeNameToKind = new Map< + string, + 'ObjectType' | 'InterfaceType' | 'UnionType' | 'EnumType' | 'InputObjectType' | 'ScalarType' + >(); + + function fillTypeNameToKindMap() { + for (const node of definitions) { + switch (node.kind) { + case Kind.OBJECT_TYPE_DEFINITION: + case Kind.OBJECT_TYPE_EXTENSION: + typeNameToKind.set(node.name.value, 'ObjectType'); + break; + case Kind.INTERFACE_TYPE_DEFINITION: + case Kind.INTERFACE_TYPE_EXTENSION: + typeNameToKind.set(node.name.value, 'InterfaceType'); + break; + case Kind.UNION_TYPE_DEFINITION: + case Kind.UNION_TYPE_EXTENSION: + typeNameToKind.set(node.name.value, 'UnionType'); + break; + case Kind.ENUM_TYPE_DEFINITION: + case Kind.ENUM_TYPE_EXTENSION: + typeNameToKind.set(node.name.value, 'EnumType'); + break; + case Kind.SCALAR_TYPE_DEFINITION: + case Kind.SCALAR_TYPE_EXTENSION: + typeNameToKind.set(node.name.value, 'ScalarType'); + break; + case Kind.INPUT_OBJECT_TYPE_DEFINITION: + case Kind.INPUT_OBJECT_TYPE_EXTENSION: + typeNameToKind.set(node.name.value, 'InputObjectType'); + break; + } + } + + filled = true; + } + + function findKindByName(typeName: string) { + if (!filled) { + fillTypeNameToKindMap(); + } + + return typeNameToKind.get(typeName); + } + + function check(node: ObjectTypeDefinitionNode | ObjectTypeExtensionNode) { + if (!node.interfaces) { + return; + } + + for (const interfaceNode of node.interfaces) { + const interfaceName = interfaceNode.name.value; + + const kind = findKindByName(interfaceName); + + if (kind && kind !== 'InterfaceType') { + context.reportError( + new GraphQLError( + `Cannot implement non-interface type ${interfaceName} (of type ObjectType)`, + { + extensions: { + code: 'INVALID_GRAPHQL', + }, + }, + ), + ); + } + } + } + + return { + ObjectTypeDefinition: check, + ObjectTypeExtension: check, + }; +} diff --git a/src/subgraph/validation/validate-subgraph.ts b/src/subgraph/validation/validate-subgraph.ts index 289cc3a..f4acc2f 100644 --- a/src/subgraph/validation/validate-subgraph.ts +++ b/src/subgraph/validation/validate-subgraph.ts @@ -40,6 +40,7 @@ import { KnownFederationDirectivesRule } from './rules/known-federation-directiv import { KnownRootTypeRule } from './rules/known-root-type-rule.js'; import { KnownTypeNamesRule } from './rules/known-type-names-rule.js'; import { LoneSchemaDefinitionRule } from './rules/lone-schema-definition-rule.js'; +import { OnlyInterfaceImplementationRule } from './rules/only-interface-implementation-rule.js'; import { ProvidedArgumentsOnDirectivesRule } from './rules/provided-arguments-on-directives-rule.js'; import { ProvidedRequiredArgumentsOnDirectivesRule } from './rules/provided-required-arguments-on-directives-rule.js'; import { QueryRootTypeInaccessibleRule } from './rules/query-root-type-inaccessible-rule.js'; @@ -157,6 +158,7 @@ export function validateSubgraph( ComposeDirectiveRules, ]; const graphqlRules = [ + OnlyInterfaceImplementationRule, LoneSchemaDefinitionRule, UniqueOperationTypesRule, UniqueTypeNamesRule, diff --git a/src/subgraph/validation/validation-context.ts b/src/subgraph/validation/validation-context.ts index 15f3f36..1d65174 100644 --- a/src/subgraph/validation/validation-context.ts +++ b/src/subgraph/validation/validation-context.ts @@ -18,6 +18,7 @@ import { TypeNodeInfo } from '../../graphql/type-node-info.js'; import { createSpecSchema, FederationVersion } from '../../specifications/federation.js'; import { LinkImport, sdl as linkSpecSdl } from '../../specifications/link.js'; import { stripTypeModifiers } from '../../utils/state.js'; +import { satisfiesVersionRange } from '../../utils/version.js'; import { TypeKind, type SubgraphStateBuilder } from '../state.js'; const linkSpec = parse(linkSpecSdl); @@ -306,19 +307,7 @@ export function createSubgraphValidationContext( return false; }, satisfiesVersionRange(range: `${'<' | '>=' | '>'} ${FederationVersion}`) { - const [sign, ver] = range.split(' ') as ['<' | '>=' | '>', FederationVersion]; - const versionInRange = parseFloat(ver.replace('v', '')); - const detectedVersion = parseFloat(version.replace('v', '')); - - if (sign === '<') { - return detectedVersion < versionInRange; - } - - if (sign === '>') { - return detectedVersion > versionInRange; - } - - return detectedVersion >= versionInRange; + return satisfiesVersionRange(version, range); }, /** * Get a list of directives defined by the spec. @@ -443,7 +432,11 @@ export function createSubgraphValidationContext( const typeDef = stateBuilder.state.types.get(typeName); - if (typeDef && typeDef.kind === TypeKind.OBJECT) { + if (!typeDef) { + return true; + } + + if (typeDef.kind === TypeKind.OBJECT && !stateBuilder.isInterfaceObject(typeName)) { const fieldDef = typeDef.fields.get(fieldName); if (fieldDef) { @@ -455,6 +448,20 @@ export function createSubgraphValidationContext( return false; } } + + for (const interfaceName of typeDef.interfaces) { + const iDef = stateBuilder.state.types.get(interfaceName); + + if (!iDef) { + continue; + } + + if (iDef.kind === TypeKind.INTERFACE && iDef.fields.has(fieldName)) { + if (iDef.fields.has(fieldName)) { + return false; + } + } + } } return true; diff --git a/src/supergraph/composition/ast.ts b/src/supergraph/composition/ast.ts index 4c74ef4..457aa5e 100644 --- a/src/supergraph/composition/ast.ts +++ b/src/supergraph/composition/ast.ts @@ -418,7 +418,7 @@ function createFieldArgumentNode(argument: { }; } -export function createJoinTypeDirectiveNode(join: { +function createJoinTypeDirectiveNode(join: { graph: string; key?: string; isInterfaceObject?: boolean; @@ -536,7 +536,7 @@ function createJoinImplementsDirectiveNode(join: { }; } -export function createJoinFieldDirectiveNode(join: { +function createJoinFieldDirectiveNode(join: { graph?: string; type?: string; override?: string; diff --git a/src/supergraph/composition/interface-type.ts b/src/supergraph/composition/interface-type.ts index 3eeff17..e6529aa 100644 --- a/src/supergraph/composition/interface-type.ts +++ b/src/supergraph/composition/interface-type.ts @@ -1,7 +1,7 @@ import { DirectiveNode } from 'graphql'; import { FederationVersion } from '../../specifications/federation.js'; import { Deprecated, Description, InterfaceType } from '../../subgraph/state.js'; -import { createInterfaceTypeNode } from './ast.js'; +import { createInterfaceTypeNode, JoinFieldAST } from './ast.js'; import { convertToConst } from './common.js'; import type { Key, MapByGraph, TypeBuilder } from './common.js'; @@ -32,6 +32,10 @@ export function interfaceTypeBuilder(): TypeBuilder { + let nonEmptyJoinField = false; + + const joinFields: JoinFieldAST[] = []; + + if (field.byGraph.size !== interfaceType.byGraph.size) { + for (const [graphId, meta] of field.byGraph.entries()) { + if ( + meta.type !== field.type || + meta.override || + meta.provides || + meta.requires || + meta.external + ) { + nonEmptyJoinField = true; + } + + joinFields.push({ + graph: graphId, + type: meta.type === field.type ? undefined : meta.type, + override: meta.override ?? undefined, + provides: meta.provides ?? undefined, + requires: meta.requires ?? undefined, + external: meta.external, + }); + } + } + return { name: field.name, type: field.type, @@ -176,19 +225,7 @@ export function interfaceTypeBuilder(): TypeBuilder ({ - graph: graphName.toUpperCase(), - type: meta.type === field.type ? undefined : meta.type, - override: meta.override ?? undefined, - provides: meta.provides ?? undefined, - requires: meta.requires ?? undefined, - })), + field: joinFields, }, }; }), @@ -210,6 +247,8 @@ export function interfaceTypeBuilder(): TypeBuilder; implementedBy: Set; fields: Map; + hasInterfaceObject: boolean; + isEntity: boolean; ast: { directives: DirectiveNode[]; }; }; -type InterfaceTypeFieldState = { +export type InterfaceTypeFieldState = { name: string; type: string; + isLeaf: boolean; tags: Set; inaccessible: boolean; authenticated: boolean; @@ -270,6 +312,7 @@ type InterfaceTypeFieldState = { scopes: string[][]; deprecated?: Deprecated; description?: Description; + usedAsKey: boolean; byGraph: MapByGraph; args: Map; ast: { @@ -295,6 +338,7 @@ type InterfaceTypeInGraph = { keys: Key[]; interfaces: Set; implementedBy: Set; + isInterfaceObject: boolean; version: FederationVersion; }; @@ -302,6 +346,9 @@ type FieldStateInGraph = { type: string; override: string | null; provides: string | null; + shareable: boolean; + usedAsKey: boolean; + external: boolean; requires: string | null; version: FederationVersion; }; @@ -328,6 +375,8 @@ function getOrCreateInterfaceType(state: Map, typeNa policies: [], scopes: [], hasDefinition: false, + hasInterfaceObject: false, + isEntity: false, byGraph: new Map(), fields: new Map(), interfaces: new Set(), @@ -356,6 +405,8 @@ function getOrCreateInterfaceField( const def: InterfaceTypeFieldState = { name: fieldName, type: fieldType, + isLeaf: false, + usedAsKey: false, tags: new Set(), inaccessible: false, authenticated: false, diff --git a/src/supergraph/composition/object-type.ts b/src/supergraph/composition/object-type.ts index dbdff84..2d8ffe7 100644 --- a/src/supergraph/composition/object-type.ts +++ b/src/supergraph/composition/object-type.ts @@ -5,6 +5,7 @@ import { isDefined } from '../../utils/helpers.js'; import { createObjectTypeNode, JoinFieldAST } from './ast.js'; import type { Key, MapByGraph, TypeBuilder } from './common.js'; import { convertToConst } from './common.js'; +import { InterfaceTypeFieldState } from './interface-type.js'; export function isRealExtension(meta: ObjectTypeStateInGraph, version: FederationVersion) { const hasExtendsDirective = meta.extensionType === '@extends'; @@ -31,9 +32,6 @@ export function isRealExtension(meta: ObjectTypeStateInGraph, version: Federatio export function objectTypeBuilder(): TypeBuilder { return { visitSubgraphState(graph, state, typeName, type) { - if (type.interfaceObjectTypeName) { // don't add this object type to supergraph schema because it's not real object (trkohler) - return; - } const objectTypeState = getOrCreateObjectType(state, typeName); type.tags.forEach(tag => objectTypeState.tags.add(tag)); @@ -131,6 +129,10 @@ export function objectTypeBuilder(): TypeBuilder { fieldState.type = field.type; } + if (field.isLeaf) { + fieldState.isLeaf = true; + } + if (!fieldState.internal.seenNonExternal && !isExternal) { fieldState.internal.seenNonExternal = true; } @@ -262,6 +264,7 @@ export function objectTypeBuilder(): TypeBuilder { const fieldNamesOfImplementedInterfaces: { [fieldName: string]: /* Graph IDs */ Set; } = {}; + const resolvableFieldsFromInterfaceObjects: InterfaceTypeFieldState[] = []; for (const interfaceName of objectType.interfaces) { const interfaceState = supergraphState.interfaceTypes.get(interfaceName); @@ -282,6 +285,14 @@ export function objectTypeBuilder(): TypeBuilder { Array.from(interfaceField.byGraph.keys()), ); } + + if (!interfaceState.hasInterfaceObject) { + continue; + } + + if (!resolvableFieldsFromInterfaceObjects.some(f => f.name === interfaceFieldName)) { + resolvableFieldsFromInterfaceObjects.push(interfaceField); + } } } @@ -647,7 +658,53 @@ export function objectTypeBuilder(): TypeBuilder { }), }; }) - .filter(isDefined), + .filter(isDefined) + .concat( + resolvableFieldsFromInterfaceObjects + .filter(f => !objectType.fields.has(f.name)) + .map(field => { + return { + name: field.name, + type: field.type, + inaccessible: field.inaccessible, + authenticated: field.authenticated, + policies: field.policies, + scopes: field.scopes, + tags: Array.from(field.tags), + description: field.description, + deprecated: field.deprecated, + ast: { + directives: convertToConst(field.ast.directives), + }, + join: { + field: [{}], + }, + arguments: Array.from(field.args.values()) + .filter(arg => { + // ignore the argument if it's not available in all subgraphs implementing the field + if (arg.byGraph.size !== field.byGraph.size) { + return false; + } + + return true; + }) + .map(arg => { + return { + name: arg.name, + type: arg.type, + inaccessible: false, + tags: Array.from(arg.tags), + defaultValue: arg.defaultValue, + description: arg.description, + deprecated: arg.deprecated, + ast: { + directives: convertToConst(arg.ast.directives), + }, + }; + }), + }; + }), + ), interfaces: Array.from(objectType.interfaces), tags: Array.from(objectType.tags), inaccessible: objectType.inaccessible, @@ -722,6 +779,7 @@ export type ObjectTypeFieldState = { type: string; tags: Set; inaccessible: boolean; + isLeaf: boolean; authenticated: boolean; policies: string[][]; scopes: string[][]; @@ -829,6 +887,7 @@ function getOrCreateField(objectTypeState: ObjectTypeState, fieldName: string, f const def: ObjectTypeFieldState = { name: fieldName, type: fieldType, + isLeaf: false, tags: new Set(), inaccessible: false, authenticated: false, diff --git a/src/supergraph/composition/visitor.ts b/src/supergraph/composition/visitor.ts index 8d9b4ee..97f955b 100644 --- a/src/supergraph/composition/visitor.ts +++ b/src/supergraph/composition/visitor.ts @@ -2,7 +2,7 @@ import { SupergraphState } from '../state.js'; import { DirectiveState } from './directive.js'; import { EnumTypeState } from './enum-type.js'; import { InputObjectTypeFieldState, InputObjectTypeState } from './input-object-type.js'; -import { InterfaceTypeState } from './interface-type.js'; +import { InterfaceTypeFieldState, InterfaceTypeState } from './interface-type.js'; import { ObjectTypeFieldArgState, ObjectTypeFieldState, ObjectTypeState } from './object-type.js'; /** @@ -75,6 +75,14 @@ export function visitSupergraphState( visitor.InterfaceType(interfaceTypeState); } } + + for (const fieldState of interfaceTypeState.fields.values()) { + for (const visitor of visitors) { + if (visitor.InterfaceTypeField) { + visitor.InterfaceTypeField(interfaceTypeState, fieldState); + } + } + } }); // Directive @@ -96,6 +104,11 @@ export interface SupergraphVisitorMap { fieldState: ObjectTypeFieldState, argState: ObjectTypeFieldArgState, ): void; + // Interface + InterfaceTypeField?( + interfaceState: InterfaceTypeState, + fieldState: InterfaceTypeFieldState, + ): void; // Enum EnumType?(enumState: EnumTypeState): void; // Input Object diff --git a/src/supergraph/validation/rules/interface-key-missing-implementation-type.ts b/src/supergraph/validation/rules/interface-key-missing-implementation-type.ts index 795b674..20c7f61 100644 --- a/src/supergraph/validation/rules/interface-key-missing-implementation-type.ts +++ b/src/supergraph/validation/rules/interface-key-missing-implementation-type.ts @@ -1,4 +1,5 @@ import { GraphQLError } from 'graphql'; +import { satisfiesVersionRange } from '../../../utils/version.js'; import { SupergraphVisitorMap } from '../../composition/visitor.js'; import { SupergraphValidationContext } from '../validation-context.js'; @@ -7,18 +8,26 @@ export function InterfaceKeyMissingImplementationTypeRule( ): SupergraphVisitorMap { return { InterfaceType(interfaceState) { - // Check first if the interface is not implemented somewhere in the supergraph. - // If at least one subgraph defines/extends the interface, but none of its object types implement it, then we look for two kinds of issues: - // - Subgraph that implements the interface needs to define all the object types that implement the interface in the supergraph. (It's madness, I know..., it shouldn't be like that, it should be ignored.) - // - Subgraph that defines/extends the interface but doesn't implement it needs to define all the object types that implement the interface in the supergraph. + if (!interfaceState.isEntity || interfaceState.hasInterfaceObject) { + // We don't need to check for this rule for non-entities or when at least one subgraph uses @interfaceObject + return; + } + let someSubgraphsAreMissingImplementation = false; for (const interfaceStateInGraph of interfaceState.byGraph.values()) { + if (satisfiesVersionRange(interfaceStateInGraph.version, '< v2.3')) { + continue; + } + if (interfaceStateInGraph.keys.length === 0) { continue; } - if (interfaceStateInGraph.implementedBy.size === 0) { + if ( + interfaceStateInGraph.implementedBy.size === 0 && + !interfaceStateInGraph.isInterfaceObject + ) { someSubgraphsAreMissingImplementation = true; break; } diff --git a/src/supergraph/validation/rules/interface-object-composition-rules.ts b/src/supergraph/validation/rules/interface-object-composition-rules.ts deleted file mode 100644 index e9244ae..0000000 --- a/src/supergraph/validation/rules/interface-object-composition-rules.ts +++ /dev/null @@ -1,272 +0,0 @@ -import { GraphQLError } from 'graphql'; -import { FederationImports } from '../../../specifications/federation.js'; -import { Field, InterfaceType, ObjectType, TypeKind } from '../../../subgraph/state.js'; -import { - allowedInterfaceObjectVersion, - importsAllowInterfaceObject, -} from '../../../subgraph/validation/rules/elements/interface-object'; -import { - createJoinFieldDirectiveNode, - createJoinTypeDirectiveNode, -} from '../../composition/ast.js'; -import { SupergraphValidationContext } from '../validation-context'; - -type TypeName = string; -type GraphName = string; -type ValidInterfaceTypeName = string; -type ObjectTypeName = string; - -type InterfaceObjectContext = { - objectType: ObjectType; - graphName: GraphName; - typeName: TypeName; -}; - -type InterfaceContext = { - interfaceType: InterfaceType; - graphName: GraphName; - graphVersion: string; - graphImports: FederationImports; - typeName: TypeName; -}; - -// if in subgraph there is interfaceObject, - -export function InterfaceObjectCompositionRule(context: SupergraphValidationContext) { - // I need to collect all types which are interfaces and objects in the different subgraphs (trkohler) - const interfaceInterfaceObjectsMap = new Map< - ValidInterfaceTypeName, - Set - >(); - const interfaceObjectContextMap = new Map(); - - const interfaces = new Map(); - - for (const [_, state] of context.subgraphStates) { - const types = state.types.values(); - for (const type of types) { - if (type.kind == TypeKind.OBJECT && type.interfaceObjectTypeName) { - interfaceObjectContextMap.set(type.name, { - objectType: type, - graphName: state.graph.id, - typeName: type.name, - }); - const exist = interfaceInterfaceObjectsMap.get(type.name); - if (exist) { - exist.add({ - objectType: type, - graphName: state.graph.id, - typeName: type.name, - }); - } else { - interfaceInterfaceObjectsMap.set( - type.name, - new Set([ - { - objectType: type, - graphName: state.graph.id, - typeName: type.name, - }, - ]), - ); - } - } - } - } - - for (const [_, state] of context.subgraphStates) { - const types = state.types.values(); - for (const type of types) { - if (type.kind == TypeKind.INTERFACE) { - const typeName = type.name; - if (interfaceObjectContextMap.get(typeName)) { - for (const interfaceObjectContext of interfaceInterfaceObjectsMap.get(typeName)!) { - const { objectType, graphName } = interfaceObjectContext; - type.interfaceObjects.set(graphName, objectType); - } - interfaces.set(typeName, { - interfaceType: type, - graphName: state.graph.id, - graphVersion: state.graph.version, - graphImports: state.graph.imports, - typeName, - }); - } - } - } - } - // validate if each interface object has its entry in the interfaceInterfaceObjectsMap (trkohler) - for (const interfaceObject of interfaceObjectContextMap.keys()) { - // this doesn't respect imports and version (trkohler) - const interfaceContext = interfaces.get(interfaceObject); - const interfaceObjectContexts = interfaceInterfaceObjectsMap.get(interfaceObject)!; - const allInterfaceObjectGraphNames = Array.from(interfaceObjectContexts).map( - interfaceObjectContext => interfaceObjectContext.graphName, - ); - const pluralGraphs = allInterfaceObjectGraphNames.length > 1; - - if (!interfaceContext) { - // interface object doesn't have corresponding interface - context.reportError( - new GraphQLError( - `@interfaceObject ${interfaceObject} in ${ - pluralGraphs ? `subgraphs` : `subgraph` - } ${allInterfaceObjectGraphNames.join( - ', ', - )} doesn't have corresponding entity interface in the different subgraph.`, - ), - ); - continue; - } - - const { - graphImports, - graphVersion, - typeName, - graphName: graphNameForInterface, - interfaceType, - } = interfaceContext; - - if ( - !allowedInterfaceObjectVersion.includes(graphVersion) || - !importsAllowInterfaceObject(graphImports) - ) { - context.reportError( - new GraphQLError( - `For @interfaceObject to work, there is must be an entity interface defined in the different subgraph. Interface ${typeName} in subgraph ${graphNameForInterface} is good candidate, but it doesn't satisfy the requirements on version (>= 2.3) or imports (@key, @interfaceObject). Maybe check those?`, - ), - ); - continue; - } - - if (interfaceType.keys.length == 0) { - context.reportError( - new GraphQLError( - `@key directive must be present on interface type ${typeName} in subgraph ${graphNameForInterface} for @objectInterface to work`, - { - extensions: { - code: 'INVALID_GRAPHQL', - }, - }, - ), - ); - } - } - - // add new fields to interfaces - const graphImplementations = new Map< - GraphName, - { - implementationsToFind: Set; - fieldsToMerge: { - field: Field; - graph: GraphName; - }[]; - } - >(); - const interfacesToModify = new Map< - ValidInterfaceTypeName, - { - fieldsToMerge: { - field: Field; - graph: GraphName; - }[]; - } - >(); - - for (const [_, interfaceContext] of interfaces) { - const { interfaceType, graphName } = interfaceContext; - const implementedBy = interfaceType.implementedBy; - const interfaceObjects = interfaceType.interfaceObjects; - let fieldsToMerge = new Map< - string, - { - field: Field; - graph: GraphName; - } - >(); - for (const [graphName, interfaceObject] of interfaceObjects) { - for (const field of interfaceObject.fields.values()) { - fieldsToMerge.set(field.name, { - field, - graph: graphName, - }); - } - } - const keyFields = interfaceType.keys; - for (const keyField of keyFields) { - const fields = keyField.fields.split(' '); - for (const field of fields) { - // triple loop!!! - fieldsToMerge.delete(field); - } - } - interfacesToModify.set(interfaceType.name, { - fieldsToMerge: Array.from(fieldsToMerge.values()), - }); - - graphImplementations.set(graphName, { - implementationsToFind: implementedBy, - fieldsToMerge: Array.from(fieldsToMerge.values()), - }); - } - - // add new fields to object type implementations - for (const [graphName, state] of context.subgraphStates) { - const implementations = graphImplementations.get(graphName); - if (!implementations) { - continue; - } - const { implementationsToFind, fieldsToMerge } = implementations; - for (const implementation of implementationsToFind) { - const objectType = state.types.get(implementation); - if (!objectType) { - // some kind of bug ? - continue; - } - - if (objectType.kind != TypeKind.OBJECT) { - // some kind of bug ? - continue; - } - for (const fieldToMerge of fieldsToMerge) { - const { field } = fieldToMerge; - const joinDirective = createJoinFieldDirectiveNode({}); - const newField = structuredClone(field); - newField.ast.directives.push(joinDirective); - objectType.fields.set(newField.name, newField); - } - } - } - - for (const [_, interfaceContext] of interfaces) { - const { interfaceType, graphName, typeName } = interfaceContext; - const { fieldsToMerge } = interfacesToModify.get(typeName)!; - const keyFields = interfaceType.keys.map(key => key.fields.split(' ')).flat(); - const foreignGraphs = []; - for (const field of interfaceType.fields.values()) { - if (keyFields.includes(field.name)) { - continue; - } - - field.ast.directives.push(createJoinFieldDirectiveNode({ graph: graphName })); - } - for (const { field, graph } of fieldsToMerge) { - // add join__field directive to each field contributed by @interfaceObject - interfaceType.fields.set(field.name, field); - field.ast.directives.push(createJoinFieldDirectiveNode({ graph })); - foreignGraphs.push(graph); - } - for (const foreignGraph of foreignGraphs) { - // add join__type directive to the interface type itself - const directive = createJoinTypeDirectiveNode({ - graph: foreignGraph, - isInterfaceObject: true, - key: keyFields.join(' '), - }); - interfaceType.ast.directives.push(directive); - } - } - - return {}; -} diff --git a/src/supergraph/validation/rules/interface-object-usage-error.ts b/src/supergraph/validation/rules/interface-object-usage-error.ts new file mode 100644 index 0000000..c85b7f8 --- /dev/null +++ b/src/supergraph/validation/rules/interface-object-usage-error.ts @@ -0,0 +1,32 @@ +import { GraphQLError } from 'graphql'; +import { SupergraphVisitorMap } from '../../composition/visitor.js'; +import { SupergraphValidationContext } from '../validation-context.js'; + +export function InterfaceObjectUsageErrorRule( + context: SupergraphValidationContext, +): SupergraphVisitorMap { + return { + InterfaceType(interfaceState) { + if (!interfaceState.hasInterfaceObject) { + return; + } + + for (const [_, interfaceStateInGraph] of interfaceState.byGraph) { + if (!interfaceStateInGraph.isInterfaceObject) { + return; + } + } + + context.reportError( + new GraphQLError( + `Type "${interfaceState.name}" is declared with @interfaceObject in all the subgraphs in which is is defined`, + { + extensions: { + code: 'INTERFACE_OBJECT_USAGE_ERROR', + }, + }, + ), + ); + }, + }; +} diff --git a/src/supergraph/validation/rules/invalid-field-sharing-rule.ts b/src/supergraph/validation/rules/invalid-field-sharing-rule.ts index caf2a7b..9e3591e 100644 --- a/src/supergraph/validation/rules/invalid-field-sharing-rule.ts +++ b/src/supergraph/validation/rules/invalid-field-sharing-rule.ts @@ -1,10 +1,12 @@ import { GraphQLError } from 'graphql'; import { andList } from '../../../utils/format.js'; import type { SupergraphVisitorMap } from '../../composition/visitor.js'; +import { SupergraphState } from '../../state.js'; import type { SupergraphValidationContext } from '../validation-context.js'; export function InvalidFieldSharingRule( context: SupergraphValidationContext, + supergraphState: SupergraphState, ): SupergraphVisitorMap { return { ObjectTypeField(objectTypeState, fieldState) { @@ -75,11 +77,151 @@ export function InvalidFieldSharingRule( resolvableIn.push(graphId); } + const interfaceObjectFieldIn: [string /* Graph ID */, string /* Interface name */][] = []; + + if (nonSharableIn.length > 0) { + // Check if there are any @interfaceObject types that implement the field + for (const interfaceName of objectTypeState.interfaces) { + const interfaceState = supergraphState.interfaceTypes.get(interfaceName); + + if (!interfaceState || !interfaceState.hasInterfaceObject) { + continue; + } + + const interfaceObjectFieldState = interfaceState.fields.get(fieldState.name); + + if (!interfaceObjectFieldState || interfaceObjectFieldState.usedAsKey) { + continue; + } + + for (const [graphId, field] of interfaceObjectFieldState.byGraph) { + const isInterfaceObject = + interfaceState.byGraph.get(graphId)?.isInterfaceObject === true; + + if (!isInterfaceObject) { + continue; + } + + const fieldIsShareable = field.shareable; + const fieldIsExternal = field.external; + const fieldHasOverride = field.override; + const fieldIsUsedAsKey = field.usedAsKey; + + if (fieldIsExternal) { + continue; + } + + if (fieldHasOverride) { + const overrideGraphId = context.graphNameToId(fieldHasOverride); + if (overrideGraphId && fieldState.byGraph.has(overrideGraphId)) { + // if a field tries to override some graph, check if it actually exists there. + // if it does, exclude it from invalid-field-sharing rule as override is effective. + continue; + } + } + + interfaceObjectFieldIn.push([graphId, interfaceName]); + + if (fieldIsShareable || fieldIsUsedAsKey) { + resolvableIn.push(graphId); + continue; + } + + nonSharableIn.push(graphId); + resolvableIn.push(graphId); + } + } + } + if (nonSharableIn.length >= 1 && resolvableIn.length > 1) { const isNonSharableInAll = resolvableIn.every(graphId => nonSharableIn.includes(graphId)); const message = `Non-shareable field "${objectTypeState.name}.${ fieldState.name + }" is resolved from multiple subgraphs: it is resolved from subgraphs ${andList( + resolvableIn.map(graphId => { + const name = context.graphIdToName(graphId); + + const interfaceObjectField = interfaceObjectFieldIn.find(([g, _]) => g === graphId); + + if (!interfaceObjectField) { + return `"${name}"`; + } + + return `"${name}" (through @interfaceObject field "${interfaceObjectField[1]}.${fieldState.name}")`; + }), + false, + )} and defined as non-shareable in ${ + isNonSharableInAll + ? 'all of them' + : `subgraph${nonSharableIn.length > 1 ? 's' : ''} ${andList( + nonSharableIn.map(context.graphIdToName), + true, + '"', + )}` + }`; + + context.reportError( + new GraphQLError(message, { + extensions: { + code: 'INVALID_FIELD_SHARING', + }, + }), + ); + } + }, + InterfaceTypeField(interfaceTypeState, fieldState) { + if (!interfaceTypeState.hasInterfaceObject) { + return; + } + + // detect a non-shareable field in a type with @interfaceObject and implementations of the interface + + const nonSharableIn: string[] = []; + const resolvableIn: string[] = []; + const interfaceObjectFieldIn: string[] = []; + + for (const [graphId, field] of fieldState.byGraph) { + const isInterfaceObject = + interfaceTypeState.byGraph.get(graphId)?.isInterfaceObject === true; + + if (!isInterfaceObject) { + continue; + } + + const fieldIsShareable = field.shareable; + const fieldIsExternal = field.external; + const fieldHasOverride = field.override; + const fieldIsUsedAsKey = field.usedAsKey; + + if (fieldIsExternal) { + continue; + } + + if (fieldHasOverride) { + const overrideGraphId = context.graphNameToId(fieldHasOverride); + if (overrideGraphId && fieldState.byGraph.has(overrideGraphId)) { + // if a field tries to override some graph, check if it actually exists there. + // if it does, exclude it from invalid-field-sharing rule as override is effective. + continue; + } + } + + if (fieldIsShareable || fieldIsUsedAsKey) { + resolvableIn.push(graphId); + continue; + } + + nonSharableIn.push(graphId); + resolvableIn.push(graphId); + interfaceObjectFieldIn.push(graphId); + } + + if (nonSharableIn.length >= 1 && resolvableIn.length > 1) { + const isNonSharableInAll = resolvableIn.every(graphId => nonSharableIn.includes(graphId)); + + const message = `Non-shareable field "${interfaceTypeState.name}.${ + fieldState.name }" is resolved from multiple subgraphs: it is resolved from subgraphs ${andList( resolvableIn.map(context.graphIdToName), false, diff --git a/src/supergraph/validation/rules/only-inaccessible-children-rule.ts b/src/supergraph/validation/rules/only-inaccessible-children-rule.ts index 8d54711..9a062fb 100644 --- a/src/supergraph/validation/rules/only-inaccessible-children-rule.ts +++ b/src/supergraph/validation/rules/only-inaccessible-children-rule.ts @@ -37,6 +37,20 @@ export function OnlyInaccessibleChildrenRule( ); } }, + InterfaceType(interfaceState) { + if (interfaceState.inaccessible === false && areAllInaccessible(interfaceState.fields)) { + context.reportError( + new GraphQLError( + `Type "${interfaceState.name}" is in the API schema but all of its fields are @inaccessible.`, + { + extensions: { + code: 'ONLY_INACCESSIBLE_CHILDREN', + }, + }, + ), + ); + } + }, InputObjectType(inputObjectTypeState) { if ( inputObjectTypeState.inaccessible === false && diff --git a/src/supergraph/validation/rules/satisfiablity-rule.ts b/src/supergraph/validation/rules/satisfiablity-rule.ts index ac5b290..83ade1c 100644 --- a/src/supergraph/validation/rules/satisfiablity-rule.ts +++ b/src/supergraph/validation/rules/satisfiablity-rule.ts @@ -1,5 +1,15 @@ -import { GraphQLError, Kind, ListValueNode, print, specifiedScalarTypes, ValueNode } from 'graphql'; +import { + GraphQLError, + Kind, + ListValueNode, + OperationTypeNode, + print, + specifiedScalarTypes, + ValueNode, +} from 'graphql'; import { isList, isNonNull, stripNonNull, stripTypeModifiers } from '../../../utils/state.js'; +import { InterfaceTypeFieldState } from '../../composition/interface-type.js'; +import { ObjectTypeFieldState } from '../../composition/object-type.js'; import type { SupergraphVisitorMap } from '../../composition/visitor.js'; import type { SupergraphState } from '../../state.js'; import type { SupergraphValidationContext } from '../validation-context.js'; @@ -26,7 +36,7 @@ export function SatisfiabilityRule( const unreachables = supergraph.validate(); - const errorByFieldCoordinate: Record = {}; + const errorByCoordinate: Record = {}; for (const unreachable of unreachables) { const edge = unreachable.superPath.edge(); @@ -38,77 +48,98 @@ export function SatisfiabilityRule( if (isFieldEdge(edge)) { const fieldCoordinate = `${edge.move.typeName}.${edge.move.fieldName}`; - if (!errorByFieldCoordinate[fieldCoordinate]) { - errorByFieldCoordinate[fieldCoordinate] = []; + if (!errorByCoordinate[fieldCoordinate]) { + errorByCoordinate[fieldCoordinate] = []; } - errorByFieldCoordinate[fieldCoordinate].push(unreachable); + errorByCoordinate[fieldCoordinate].push(unreachable); + } else { + const coordinate = edge.head.typeName; + + if (!errorByCoordinate[coordinate]) { + errorByCoordinate[coordinate] = []; + } + + errorByCoordinate[coordinate].push(unreachable); } } - return { - ObjectTypeField(objectState, fieldState) { - const coordinate = `${objectState.name}.${fieldState.name}`; + function check(typeName: string, fieldName?: string) { + const coordinate = fieldName ? `${typeName}.${fieldName}` : typeName; - const unreachables = errorByFieldCoordinate[coordinate]; + const unreachables = errorByCoordinate[coordinate]; - if (!unreachables?.length) { - return; - } - - for (const unreachable of unreachables) { - const queryString = printQueryPath(supergraphState, unreachable.superPath.steps()); + if (!unreachables?.length) { + return; + } - if (!queryString) { - return; - } + for (const unreachable of unreachables) { + const queryString = printQueryPath(supergraphState, unreachable.superPath.steps()); - const errorsBySourceGraph: Record = {}; - const reasons: Array<[string, string[]]> = []; + if (!queryString) { + return; + } - for (const error of unreachable.listErrors()) { - const sourceGraphName = error.sourceGraphName; + const errorsBySourceGraph: Record = {}; + const reasons: Array<[string, string[]]> = []; - if (!errorsBySourceGraph[sourceGraphName]) { - errorsBySourceGraph[sourceGraphName] = []; - } + for (const error of unreachable.listErrors()) { + const sourceGraphName = error.sourceGraphName; - errorsBySourceGraph[sourceGraphName].push(error); + if (!errorsBySourceGraph[sourceGraphName]) { + errorsBySourceGraph[sourceGraphName] = []; } - for (const sourceGraphName in errorsBySourceGraph) { - const errors = errorsBySourceGraph[sourceGraphName]; - reasons.push([sourceGraphName, errors.map(e => e.message)]); - } + errorsBySourceGraph[sourceGraphName].push(error); + } - if (reasons.length === 0) { - continue; - } + for (const sourceGraphName in errorsBySourceGraph) { + const errors = errorsBySourceGraph[sourceGraphName]; + reasons.push([sourceGraphName, errors.map(e => e.message)]); + } - context.reportError( - new GraphQLError( - [ - 'The following supergraph API query:', - queryString, - 'cannot be satisfied by the subgraphs because:', - ...reasons.map(([graphName, reasons]) => { - if (reasons.length === 1) { - return `- from subgraph "${graphName}": ${reasons[0]}`; - } - - return ( - `- from subgraph "${graphName}":\n` + reasons.map(r => ` - ${r}`).join('\n') - ); - }), - ].join('\n'), - { - extensions: { - code: 'SATISFIABILITY_ERROR', - }, - }, - ), - ); + if (reasons.length === 0) { + continue; } + + context.reportError( + new GraphQLError( + [ + 'The following supergraph API query:', + queryString, + 'cannot be satisfied by the subgraphs because:', + ...reasons.map(([graphName, reasons]) => { + if (reasons.length === 1) { + return `- from subgraph "${graphName}": ${reasons[0]}`; + } + + return `- from subgraph "${graphName}":\n` + reasons.map(r => ` - ${r}`).join('\n'); + }), + ].join('\n'), + { + extensions: { + code: 'SATISFIABILITY_ERROR', + }, + }, + ), + ); + } + } + + return { + InterfaceType(interfaceState) { + check(interfaceState.name); + interfaceState.implementedBy.forEach(typeName => check(typeName)); + }, + ObjectType(objectState) { + check(objectState.name); + }, + InterfaceTypeField(interfaceState, fieldState) { + check(interfaceState.name, fieldState.name); + interfaceState.implementedBy.forEach(typeName => check(typeName, fieldState.name)); + }, + ObjectTypeField(objectState, fieldState) { + check(objectState.name, fieldState.name); }, }; } @@ -126,9 +157,30 @@ function printQueryPath(supergraphState: SupergraphState, queryPath: QueryPath) const point = queryPath[i]; if ('fieldName' in point) { - const fieldState = supergraphState.objectTypes - .get(point.typeName) - ?.fields.get(point.fieldName); + const typeState = supergraphState.objectTypes.get(point.typeName); + + if (!typeState) { + throw new Error(`Object type "${point.typeName}" not found in Supergraph state`); + } + + let fieldState: ObjectTypeFieldState | InterfaceTypeFieldState | undefined = + typeState?.fields.get(point.fieldName); + + if (!fieldState) { + for (const interfaceName of typeState.interfaces) { + const interfaceState = supergraphState.interfaceTypes.get(interfaceName); + + if (!interfaceState) { + throw new Error(`Interface type "${interfaceName}" not found in Supergraph state`); + } + + fieldState = interfaceState.fields.get(point.fieldName); + + if (fieldState) { + break; + } + } + } if (!fieldState) { throw new Error( diff --git a/src/supergraph/validation/rules/satisfiablity/errors.ts b/src/supergraph/validation/rules/satisfiablity/errors.ts index 467b36e..7ebd779 100644 --- a/src/supergraph/validation/rules/satisfiablity/errors.ts +++ b/src/supergraph/validation/rules/satisfiablity/errors.ts @@ -3,7 +3,8 @@ type SatisfiabilityErrorKind = | 'REQUIRE' // cannot satisfy @require conditions on field "User.name". | 'EXTERNAL' // field "User.name" is not resolvable because marked @external | 'MISSING_FIELD' // cannot find field "User.name". - | 'NO_KEY'; // cannot move to subgraph "X", which has field "User.name", because type "User" has no @key defined in subgraph "Y". + | 'NO_KEY' // cannot move to subgraph "X", which has field "User.name", because type "User" has no @key defined in subgraph "Y". + | 'NO_IMPLEMENTATION'; // no subgraph can be reached to resolve the implementation type of @interfaceObject type "X". export class SatisfiabilityError extends Error { static forKey( @@ -77,6 +78,17 @@ export class SatisfiabilityError extends Error { `cannot move to subgraph "${targetGraphName}", which has field "${typeName}.${fieldName}", because type "${typeName}" has no @key defined in subgraph "${targetGraphName}".`, ); } + + static forNoImplementation(sourceGraphName: string, typeName: string): SatisfiabilityError { + return new SatisfiabilityError( + 'NO_IMPLEMENTATION', + sourceGraphName, + typeName, + null, + `no subgraph can be reached to resolve the implementation type of @interfaceObject type "${typeName}".`, + ); + } + private constructor( public kind: SatisfiabilityErrorKind, public sourceGraphName: string, diff --git a/src/supergraph/validation/rules/satisfiablity/finder.ts b/src/supergraph/validation/rules/satisfiablity/finder.ts index 5b1ecf0..28f7954 100644 --- a/src/supergraph/validation/rules/satisfiablity/finder.ts +++ b/src/supergraph/validation/rules/satisfiablity/finder.ts @@ -1,10 +1,10 @@ import type { Logger } from '../../../../utils/logger.js'; import { Edge, isAbstractEdge, isEntityEdge, isFieldEdge } from './edge.js'; import { SatisfiabilityError } from './errors.js'; -import { Fields } from './fields.js'; import type { Graph } from './graph.js'; import type { MoveValidator } from './move-validator.js'; import type { OperationPath } from './operation-path.js'; +import { Selection } from './selection.js'; export function concatIfNotExistsString(list: string[], item: string): string[] { if (list.includes(item)) { @@ -14,7 +14,7 @@ export function concatIfNotExistsString(list: string[], item: string): string[] return list.concat(item); } -export function concatIfNotExistsFields(list: Fields[], item: Fields): Fields[] { +export function concatIfNotExistsFields(list: Selection[], item: Selection): Selection[] { if (list.some(f => f.equals(item))) { return list; } @@ -83,12 +83,7 @@ export class PathFinder { } } - if ( - isFieldTarget && - isFieldEdge(edge) && - edge.move.typeName === typeName && - edge.move.fieldName === fieldName - ) { + if (isFieldTarget && isFieldEdge(edge) && edge.move.fieldName === fieldName) { const resolvable = this.moveValidator.isEdgeResolvable(edge, path, [], [], []); if (!resolvable.success) { errors.push(resolvable.error); @@ -107,77 +102,244 @@ export class PathFinder { this.logger.groupEnd(() => 'Found ' + nextPaths.length + ' direct paths'); - // TODO: we will have to adjust pushed errors as we moved Abstract and Field moves here, to direct paths finder. - if (nextPaths.length === 0) { - // In case of no errors, we know that there were no edges matching the field name. - if (errors.length === 0) { - if (isFieldTarget) { - errors.push(SatisfiabilityError.forMissingField(tail.graphName, typeName, fieldName)); - - // find graphs with the same type and field name, but no @key defined - const typeNodes = this.graph.nodesOf(typeName); - for (const typeNode of typeNodes) { - const edges = this.graph.fieldEdgesOfHead(typeNode, fieldName); - for (const edge of edges) { - if ( - isFieldEdge(edge) && - edge.move.typeName === typeName && - edge.move.fieldName === fieldName && - !this.moveValidator.isExternal(edge) - ) { - const typeStateInGraph = - edge.head.typeState && - edge.head.typeState.kind === 'object' && - edge.head.typeState.byGraph.get(edge.head.graphId); - const keys = typeStateInGraph - ? typeStateInGraph.keys.filter(key => key.resolvable) - : []; - - if (keys.length === 0) { - errors.push( - SatisfiabilityError.forNoKey( - tail.graphName, - edge.tail.graphName, - typeName, - fieldName, - ), - ); - } - } - } - } - } else { - // This is a special case where we are looking for an abstract type, but there are no edges leading to it. - // It's completely fine, as abstract types are not resolvable by themselves and Federation will handle it (return empty result). + if (nextPaths.length > 0) { + return { + success: true, + paths: nextPaths, + errors: undefined, + }; + } + + if (errors.length > 0) { + return { + success: false, + errors, + paths: undefined, + }; + } + + if (!isFieldTarget) { + if (tail.typeState?.kind === 'interface' && tail.typeState.hasInterfaceObject) { + const typeStateInGraph = tail.typeState.byGraph.get(tail.graphId); + + if (typeStateInGraph?.isInterfaceObject) { + // no subgraph can be reached to resolve the implementation type of @interfaceObject type return { - success: true, - errors: undefined, - paths: [], + success: false, + errors: [SatisfiabilityError.forNoImplementation(tail.graphName, tail.typeName)], + paths: undefined, }; } } + // This is a special case where we are looking for an abstract type, but there are no edges leading to it. + // It's completely fine, as abstract types are not resolvable by themselves and Federation will handle it (return empty result). return { - success: false, - errors, - paths: undefined, + success: true, + errors: undefined, + paths: [], }; } + // In case of no errors, we know that there were no edges matching the field name. + errors.push(SatisfiabilityError.forMissingField(tail.graphName, typeName, fieldName)); + + // find graphs with the same type and field name, but no @key defined + const typeNodes = this.graph.nodesOf(typeName); + for (const typeNode of typeNodes) { + const edges = this.graph.fieldEdgesOfHead(typeNode, fieldName); + for (const edge of edges) { + if ( + isFieldEdge(edge) && + // edge.move.typeName === typeName && + edge.move.fieldName === fieldName && + !this.moveValidator.isExternal(edge) + ) { + const typeStateInGraph = + edge.head.typeState && + edge.head.typeState.kind === 'object' && + edge.head.typeState.byGraph.get(edge.head.graphId); + const keys = typeStateInGraph ? typeStateInGraph.keys.filter(key => key.resolvable) : []; + + if (keys.length === 0) { + errors.push( + SatisfiabilityError.forNoKey( + tail.graphName, + edge.tail.graphName, + typeName, + fieldName, + ), + ); + } + } + } + } + return { - success: true, - paths: nextPaths, - errors: undefined, + success: false, + errors, + paths: undefined, }; } + private findFieldIndirectly( + path: OperationPath, + typeName: string, + fieldName: string, + visitedEdges: Edge[], + visitedGraphs: string[], + visitedFields: Selection[], + errors: SatisfiabilityError[], + finalPaths: OperationPath[], + queue: [string[], Selection[], OperationPath][], + shortestPathPerGraph: Map, + edge: Edge, + ) { + if (!isEntityEdge(edge) && !isAbstractEdge(edge)) { + this.logger.groupEnd(() => 'Ignored'); + return; + } + + const shortestPathToThisGraph = shortestPathPerGraph.get(edge.tail.graphName); + if (shortestPathToThisGraph && shortestPathToThisGraph.depth() <= path.depth()) { + this.logger.groupEnd(() => 'Already found a shorter path to ' + edge.tail); + return; + } + + // A huge win for performance, is when you do less work :D + // We can ignore an edge that has already been visited with the same key fields / requirements. + // The way entity-move edges are created, where every graph points to every other graph: + // Graph A: User @key(id) @key(name) + // Graph B: User @key(id) + // Edges in a merged graph: + // - User/A @key(id) -> User/B + // - User/B @key(id) -> User/A + // - User/B @key(name) -> User/A + // Allows us to ignore an edge with the same key fields. + // That's because in some other path, we will or already have checked the other edge. + if (!!edge.move.keyFields && visitedFields.some(f => f.equals(edge.move.keyFields!))) { + this.logger.groupEnd(() => 'Ignore: already visited fields'); + return; + } + + if (isAbstractEdge(edge)) { + // prevent a situation where we are doing a second abstract move + const tailEdge = path.edge(); + + if (tailEdge && isAbstractEdge(tailEdge) && !tailEdge.move.keyFields) { + this.logger.groupEnd(() => 'Ignore: cannot do two abstract moves in a row'); + return; + } + + if (!edge.isCrossGraphEdge()) { + const newPath = path.clone().move(edge); + queue.push([visitedGraphs, visitedFields, newPath]); + this.logger.log(() => 'Abstract move'); + this.logger.groupEnd(() => 'Adding to queue: ' + newPath); + return; + } + } + + const resolvable = this.moveValidator.isEdgeResolvable( + edge, + path, + visitedEdges.concat(edge), + visitedGraphs, + visitedFields, + ); + + if (!resolvable.success) { + errors.push(resolvable.error); + this.logger.groupEnd(() => 'Not resolvable: ' + resolvable.error); + return; + } + + const newPath = path.clone().move(edge); + + this.logger.log( + () => + 'From indirect path, look for direct paths to ' + + typeName + + '.' + + fieldName + + ' from: ' + + edge, + ); + const direct = this.findDirectPaths(newPath, typeName, fieldName, [edge]); + + if (direct.success) { + this.logger.groupEnd(() => 'Resolvable: ' + edge + ' with ' + direct.paths.length + ' paths'); + + finalPaths.push(...direct.paths); + return; + } + + errors.push(...direct.errors); + + setShortest(newPath, shortestPathPerGraph); + + queue.push([ + concatIfNotExistsString(visitedGraphs, edge.tail.graphName), + 'keyFields' in edge.move && edge.move.keyFields + ? concatIfNotExistsFields(visitedFields, edge.move.keyFields) + : visitedFields, + newPath, + ]); + this.logger.log(() => 'Did not find direct paths'); + this.logger.groupEnd(() => 'Adding to queue: ' + newPath); + } + + private findTypeIndirectly( + path: OperationPath, + typeName: string, + visitedGraphs: string[], + visitedFields: Selection[], + finalPaths: OperationPath[], + queue: [string[], Selection[], OperationPath][], + shortestPathPerGraph: Map, + edge: Edge, + ) { + if (!isAbstractEdge(edge)) { + this.logger.groupEnd(() => 'Ignored'); + return; + } + + if (shortestPathPerGraph.has(edge.tail.graphName)) { + this.logger.groupEnd(() => 'Already found a shorter path to ' + edge.tail); + return; + } + + if (edge.move.keyFields && visitedFields.some(f => f.equals(edge.move.keyFields!))) { + this.logger.groupEnd(() => 'Ignore: already visited fields'); + return; + } + + const newPath = path.clone().move(edge); + + // If the target is the tail of this edge, we have found a path + if (edge.tail.typeName === typeName) { + setShortest(newPath, shortestPathPerGraph); + finalPaths.push(newPath); + } else { + // Otherwise, we need to continue searching for the target + queue.push([ + visitedGraphs, + edge.move.keyFields + ? concatIfNotExistsFields(visitedFields, edge.move.keyFields) + : visitedFields, + newPath, + ]); + } + this.logger.groupEnd(() => 'Resolvable'); + } + findIndirectPaths( path: OperationPath, typeName: string, fieldName: string | null, visitedEdges: Edge[], visitedGraphs: string[], - visitedFields: Fields[], + visitedFields: Selection[], ): PathFinderResult { const errors: SatisfiabilityError[] = []; const tail = path.tail() ?? path.rootNode(); @@ -187,10 +349,9 @@ export class PathFinder { this.logger.group(() => 'Indirect paths to ' + id + ' from: ' + tail); - const queue: [string[], Fields[], OperationPath][] = [[visitedGraphs, visitedFields, path]]; + const queue: [string[], Selection[], OperationPath][] = [[visitedGraphs, visitedFields, path]]; const finalPaths: OperationPath[] = []; const shortestPathPerGraph = new Map(); - const edgesToIgnore: Edge[] = visitedEdges.slice(); while (queue.length > 0) { const item = queue.pop(); @@ -201,12 +362,7 @@ export class PathFinder { const [visitedGraphs, visitedFields, path] = item; const tail = path.tail() ?? path.rootNode(); - const edges = this.graph.crossGraphEdgesOfHead(tail); - - if (!this.graph.canReachTypeFromType(tail.typeName, typeName)) { - this.logger.log(() => 'Cannot reach ' + typeName + ' from ' + tail.typeName); - continue; - } + const edges = this.graph.indirectEdgesOfHead(tail); this.logger.log(() => 'At path: ' + path); this.logger.log(() => 'Checking ' + edges.length + ' edges'); @@ -220,12 +376,7 @@ export class PathFinder { continue; } - if (!edge.isCrossGraphEdge()) { - this.logger.groupEnd(() => 'Not cross-graph edge'); - continue; - } - - if (edgesToIgnore.includes(edge)) { + if (visitedEdges.includes(edge)) { this.logger.groupEnd(() => 'Ignore: already visited edge'); continue; } @@ -237,119 +388,31 @@ export class PathFinder { continue; } - if (isFieldTarget && isEntityEdge(edge)) { - if (visitedFields.some(f => f.equals(edge.move.keyFields))) { - // A huge win for performance, is when you do less work :D - // We can ignore an edge that has already been visited with the same key fields / requirements. - // The way entity-move edges are created, where every graph points to every other graph: - // Graph A: User @key(id) @key(name) - // Graph B: User @key(id) - // Edges in a merged graph: - // - User/A @key(id) -> User/B - // - User/B @key(id) -> User/A - // - User/B @key(name) -> User/A - // Allows us to ignore an edge with the same key fields. - // That's because in some other path, we will or already have checked the other edge. - this.logger.groupEnd(() => 'Ignore: already visited fields'); - continue; - } - - const shortestPathToThisGraph = shortestPathPerGraph.get(edge.tail.graphName); - if (shortestPathToThisGraph && shortestPathToThisGraph.depth() <= path.depth()) { - this.logger.groupEnd(() => 'Already found a shorter path to ' + edge.tail); - continue; - } - - const resolvable = this.moveValidator.isEdgeResolvable( - edge, + if (isFieldTarget) { + this.findFieldIndirectly( path, - edgesToIgnore.concat(edge), + typeName, + fieldName, + visitedEdges, visitedGraphs, visitedFields, - ); - - if (!resolvable.success) { - errors.push(resolvable.error); - this.logger.groupEnd(() => 'Not resolvable: ' + resolvable.error); - continue; - } - - const newPath = path.clone().move(edge); - - this.logger.log( - () => 'From indirect path, look for direct paths to ' + id + ' from: ' + edge, - ); - const direct = this.findDirectPaths(newPath, typeName, fieldName, [edge]); - - if (direct.success) { - this.logger.groupEnd( - () => 'Resolvable: ' + edge + ' with ' + direct.paths.length + ' paths', - ); - - finalPaths.push(...direct.paths); - continue; - } - - errors.push(...direct.errors); - - setShortest(newPath, shortestPathPerGraph); - - queue.push([ - concatIfNotExistsString(visitedGraphs, edge.tail.graphName), - concatIfNotExistsFields(visitedFields, edge.move.keyFields), - newPath, - ]); - this.logger.log(() => 'Did not find direct paths'); - this.logger.groupEnd(() => 'Adding to queue: ' + newPath); - } else if (isFieldTarget && isFieldEdge(edge)) { - this.logger.log(() => 'Cross graph field move:' + edge.move); - if (path.isVisitedEdge(edge)) { - this.logger.groupEnd(() => 'Already visited'); - continue; - } - - if (isFieldTarget && edge.move.requires?.contains(typeName, fieldName)) { - errors.push(SatisfiabilityError.forRequire(tail.graphName, typeName, fieldName)); - this.logger.groupEnd(() => 'Ignored'); - continue; - } - - if (edge.move.requires && visitedFields.some(f => f.equals(edge.move.requires!))) { - // double check if we should ignore it for non field target - this.logger.groupEnd(() => 'Ignore: already visited fields'); - continue; - } - - const resolvable = this.moveValidator.isEdgeResolvable( + errors, + finalPaths, + queue, + shortestPathPerGraph, edge, + ); + } else { + this.findTypeIndirectly( path, - visitedEdges.concat(edge), + typeName, visitedGraphs, - edge.move.requires - ? concatIfNotExistsFields(visitedFields, edge.move.requires) - : visitedFields, + visitedFields, + finalPaths, + queue, + shortestPathPerGraph, + edge, ); - - if (!resolvable.success) { - errors.push(resolvable.error); - this.logger.groupEnd(() => 'Not resolvable: ' + resolvable.error); - continue; - } - - setShortest(path.clone().move(edge), shortestPathPerGraph); - this.logger.groupEnd(() => 'Resolvable: ' + edge); - } else if (!isFieldTarget && isAbstractEdge(edge)) { - if (shortestPathPerGraph.has(edge.tail.graphName)) { - this.logger.groupEnd(() => 'Already found a shorter path to ' + edge.tail); - continue; - } - - const newPath = path.clone().move(edge); - setShortest(newPath, shortestPathPerGraph); - finalPaths.push(newPath); - this.logger.groupEnd(() => 'Resolvable'); - } else { - this.logger.groupEnd(() => 'Ignored...'); } } } diff --git a/src/supergraph/validation/rules/satisfiablity/graph.ts b/src/supergraph/validation/rules/satisfiablity/graph.ts index 2237b4f..013772c 100644 --- a/src/supergraph/validation/rules/satisfiablity/graph.ts +++ b/src/supergraph/validation/rules/satisfiablity/graph.ts @@ -2,7 +2,10 @@ import { specifiedScalarTypes } from 'graphql'; import type { Logger } from '../../../../utils/logger.js'; import { stripTypeModifiers } from '../../../../utils/state.js'; import type { EnumTypeState } from '../../../composition/enum-type.js'; -import type { InterfaceTypeState } from '../../../composition/interface-type.js'; +import type { + InterfaceTypeFieldState, + InterfaceTypeState, +} from '../../../composition/interface-type.js'; import type { ObjectTypeFieldState, ObjectTypeState } from '../../../composition/object-type.js'; import type { ScalarTypeState } from '../../../composition/scalar-type.js'; import type { UnionTypeState } from '../../../composition/union-type.js'; @@ -16,10 +19,10 @@ import { isEntityEdge, isFieldEdge, } from './edge.js'; -import type { Field, FieldsResolver } from './fields.js'; import { scoreKeyFields } from './helpers.js'; import { AbstractMove, EntityMove, FieldMove } from './moves.js'; import { Node } from './node.js'; +import type { SelectionNode, SelectionResolver } from './selection.js'; export class Graph { private _warnedAboutIncorrectEdge = false; @@ -49,7 +52,7 @@ export class Graph { id: string | Symbol, public name: string, private supergraphState: SupergraphState, - private fieldsResolver: FieldsResolver, + private selectionResolver: SelectionResolver, private ignoreInaccessible = false, ) { this.logger = logger.create('Graph'); @@ -112,8 +115,56 @@ export class Graph { return this; } + /** + * Add fields from @interfaceObject types + */ + addInterfaceObjectFields() { + if (this.isSubgraph) { + throw new Error('Expected to be called only on supergraph'); + } + + for (const interfaceState of this.supergraphState.interfaceTypes.values()) { + if (!interfaceState.hasInterfaceObject) { + // This is a regular interface, we don't need to do anything + continue; + } + + for (const implementedBy of interfaceState.implementedBy) { + const objectState = this.supergraphState.objectTypes.get(implementedBy); + + if (!objectState) { + throw new Error( + `Expected object type ${implementedBy} to be defined as it implements ${interfaceState.name}`, + ); + } + + // If a Node is not defined, it means it's not reachable from the root as we first start from the root and add all reachable nodes + const head = this.nodeOf(objectState.name, false); + + if (!head) { + continue; + } + + for (const [interfaceFieldName, interfaceField] of interfaceState.fields) { + if (objectState.fields.has(interfaceFieldName)) { + // It already has a field with the same name, we don't need to do anything + continue; + } + + // even though it's an object type... + this.createEdgeForInterfaceTypeField(head, interfaceField); + } + } + + for (const [typeName, state] of this.supergraphState.objectTypes) { + if (state.byGraph.has(this.id)) { + this.createNodesAndEdgesForType(typeName); + } + } + } + } + addFromEntities() { - // TODO: support entity interfaces (if necessary... haven't seen anything broken yet) for (const typeState of this.supergraphState.objectTypes.values()) { if (typeState?.isEntity && this.trueOrIfSubgraphThen(() => typeState.byGraph.has(this.id))) { this.createNodesAndEdgesForType(typeState.name); @@ -193,7 +244,11 @@ export class Graph { edgesToAdd.push( new Edge( headNode, - new EntityMove(this.fieldsResolver.resolve(headNode.typeName, key.fields)), + tailNode.typeState.kind === 'object' + ? new EntityMove(this.selectionResolver.resolve(headNode.typeName, key.fields)) + : new AbstractMove( + this.selectionResolver.resolve(headNode.typeName, key.fields), + ), tailNode, ), ); @@ -206,10 +261,10 @@ export class Graph { private addProvidedInterfaceFields( head: Node, - providedFields: Field[], + providedFields: SelectionNode[], queue: { head: Node; - providedFields: Field[]; + providedFields: SelectionNode[]; }[], ) { const abstractIndexes = head.getAbstractEdgeIndexes(head.typeName); @@ -218,9 +273,9 @@ export class Graph { throw new Error('Expected abstract indexes to be defined'); } - const interfaceFields: Field[] = []; + const interfaceFields: SelectionNode[] = []; - const fieldsByType = new Map(); + const fieldsByType = new Map(); for (const providedField of providedFields) { if (providedField.typeName === head.typeName) { @@ -302,14 +357,22 @@ export class Graph { private addProvidedField( head: Node, - providedField: Field, + providedField: SelectionNode, queue: { head: Node; - providedFields: Field[]; + providedFields: SelectionNode[]; }[], ) { // As we only need to check if all fields are reachable from the head, we can ignore __typename - if (providedField.fieldName === '__typename') { + if (providedField.kind === 'field' && providedField.fieldName === '__typename') { + return; + } + + if (providedField.kind === 'fragment') { + queue.push({ + head, + providedFields: providedField.selectionSet, + }); return; } @@ -384,8 +447,71 @@ export class Graph { continue; } - if (typeNode.typeState?.kind === 'object' && typeNode.typeState?.isEntity) { + if ( + (typeNode.typeState?.kind === 'object' || typeNode.typeState?.kind === 'interface') && + typeNode.typeState?.isEntity + ) { this.connectEntities(i, otherNodesIndexes, edgesToAdd); + + for (const h of otherNodesIndexes) { + const head = this.nodesByTypeIndex[h][0]; + + // For example, if we have + // Subgraph 1: + // type A @interfaceObject @key(fields: "id") {} + // Subgraph 2: + // interface A @key(fields: "id") {} + // type B implements A @key(fields: "id") {} + // We need to add an edge from A to B + // + // The general idea here is to allow to move from a type that implements an interface to another type that implements the same interface + + for (const interfaceName of typeNode.typeState.interfaces) { + const interfaceNodes = this.nodesOf(interfaceName, false); + + if (interfaceNodes.length === 0) { + continue; + } + + const interfaceTypeNode = interfaceNodes[0]; + + if (!interfaceTypeNode.typeState || interfaceTypeNode.typeState?.kind !== 'interface') { + continue; + } + + if (!interfaceTypeNode.typeState.hasInterfaceObject) { + continue; + } + + for (const interfaceNode of interfaceNodes) { + if (interfaceNode.typeState?.kind !== 'interface') { + throw new Error( + `Expected interfaceNode ${interfaceNode.toString()} to be an interface`, + ); + } + + const keys = interfaceNode.typeState.byGraph.get(interfaceNode.graphId)?.keys; + + if (!keys || keys.length === 0) { + continue; + } + + for (const key of keys) { + if (!key.resolvable) { + continue; + } + + edgesToAdd.push( + new Edge( + head, + new AbstractMove(this.selectionResolver.resolve(interfaceName, key.fields)), + interfaceNode, + ), + ); + } + } + } + } } else if (typeNode.typeState.kind === 'union' || typeNode.typeState.kind === 'interface') { this.connectUnionOrInterface(i, otherNodesIndexes, edgesToAdd); } @@ -425,11 +551,11 @@ export class Graph { const queue: { head: Node; - providedFields: Field[]; + providedFields: SelectionNode[]; }[] = [ { head: newTail, - providedFields: edge.move.provides.fields, + providedFields: edge.move.provides.selectionSet, }, ]; @@ -530,6 +656,9 @@ export class Graph { } } + nodeOf(typeName: string): Node; + nodeOf(typeName: string, failIfMissing: true): Node; + nodeOf(typeName: string, failIfMissing?: false): Node | undefined; nodeOf(typeName: string, failIfMissing = true) { const indexes = this.getIndexesOfType(typeName); @@ -618,12 +747,16 @@ export class Graph { return this.getSameGraphEdgesOfIndex(head, head.getEntityEdgeIndexes(head.typeName), 'entity'); } - crossGraphEdgesOfHead(head: Node) { + indirectEdgesOfHead(head: Node) { return this.getSameGraphEdgesOfIndex( head, head.getCrossGraphEdgeIndexes(head.typeName), 'cross-graph', - ); + ) + .concat( + this.getSameGraphEdgesOfIndex(head, head.getAbstractEdgeIndexes(head.typeName), 'abstract'), + ) + .filter((edge, i, all) => all.indexOf(edge) === i); } edgesOfHead(head: Node) { @@ -829,9 +962,55 @@ export class Graph { } } + if (typeState.hasInterfaceObject && this.isSubgraph) { + // We need to add fields of interface types with @interfaceObject + // but only add fields that are owned by the subgraph. + for (const field of typeState.fields.values()) { + if (field.byGraph.has(this.id)) { + this.createEdgeForInterfaceTypeField(head, field); + } + } + } else if (this.isSubgraph) { + // We're adding just leafs + for (const field of typeState.fields.values()) { + if (field.isLeaf && field.byGraph.has(this.id)) { + this.createEdgeForInterfaceTypeField(head, field); + } + } + } + return head; } + private createEdgeForInterfaceTypeField(head: Node, field: InterfaceTypeFieldState) { + if (this.ignoreInaccessible && field.inaccessible) { + return; + } + + const outputTypeName = stripTypeModifiers(field.type); + const tail = this.createNodesAndEdgesForType(outputTypeName); + + if (!tail) { + throw new Error(`Failed to create Node for ${outputTypeName} in subgraph ${this.id}`); + } + + const requires = field.byGraph.get(head.graphId)?.requires; + const provides = field.byGraph.get(head.graphId)?.provides; + + return this.addEdge( + new Edge( + head, + new FieldMove( + head.typeName, + field.name, + requires ? this.selectionResolver.resolve(head.typeName, requires) : null, + provides ? this.selectionResolver.resolve(outputTypeName, provides) : null, + ), + tail, + ), + ); + } + private createEdgeForObjectTypeField(head: Node, field: ObjectTypeFieldState) { if (this.ignoreInaccessible && field.inaccessible) { return; @@ -867,8 +1046,8 @@ export class Graph { new FieldMove( head.typeName, field.name, - requires ? this.fieldsResolver.resolve(head.typeName, requires) : null, - provides ? this.fieldsResolver.resolve(outputTypeName, provides) : null, + requires ? this.selectionResolver.resolve(head.typeName, requires) : null, + provides ? this.selectionResolver.resolve(outputTypeName, provides) : null, ), tail, ), diff --git a/src/supergraph/validation/rules/satisfiablity/move-validator.ts b/src/supergraph/validation/rules/satisfiablity/move-validator.ts index 778ebd1..d89e615 100644 --- a/src/supergraph/validation/rules/satisfiablity/move-validator.ts +++ b/src/supergraph/validation/rules/satisfiablity/move-validator.ts @@ -1,30 +1,30 @@ import type { Logger } from '../../../../utils/logger.js'; -import { Edge, isEntityEdge, isFieldEdge } from './edge.js'; +import { Edge, isAbstractEdge, isEntityEdge, isFieldEdge } from './edge.js'; import { SatisfiabilityError } from './errors.js'; -import type { Field, Fields } from './fields.js'; import { concatIfNotExistsFields, concatIfNotExistsString, PathFinder } from './finder.js'; import type { Graph } from './graph.js'; import { OperationPath } from './operation-path.js'; +import type { Field, Fragment, Selection, SelectionNode } from './selection.js'; -type MoveRequirement = { +type MoveRequirement = { paths: OperationPath[]; -} & ( - | { - field: Field; - } - | { - type: { - parentTypeName: string; - childTypeName: string; - field: Field; - }; - } -); + selection: T; +}; + +function isFragmentRequirement( + requirement: MoveRequirement, +): requirement is MoveRequirement { + return requirement.selection.kind === 'fragment'; +} + +function isFieldRequirement(requirement: MoveRequirement): requirement is MoveRequirement { + return requirement.selection.kind === 'field'; +} -type EdgeResolvabilityResult = +type RequirementResult = | { success: true; - errors: undefined; + requirements: MoveRequirement[]; } | { success: false; @@ -32,7 +32,6 @@ type EdgeResolvabilityResult = }; export class MoveValidator { - private cache: Map = new Map(); private logger: Logger; private pathFinder: PathFinder; @@ -44,41 +43,22 @@ export class MoveValidator { this.pathFinder = new PathFinder(this.logger, supergraph, this); } - private canResolveFields( - fields: Field[], + private canResolveSelectionSet( + selectionSet: SelectionNode[], path: OperationPath, visitedEdges: Edge[], visitedGraphs: string[], - visitedFields: Fields[], - ): EdgeResolvabilityResult { - // TODO: adjust cache key to have required fields instead of edge.move - const cacheKey = - JSON.stringify(fields) + - ' | ' + - visitedGraphs.join(',') + - visitedFields.join(',') + - ' | ' + - visitedEdges - .map(e => e.toString()) - .sort() - .join(','); - const cached = this.cache.get(cacheKey); - - if (cached) { - return cached; - } - + visitedFields: Selection[], + ) { const requirements: MoveRequirement[] = []; - for (const field of fields) { + for (const selection of selectionSet) { requirements.unshift({ - field, + selection, paths: [path.clone()], }); } - // it should look for complex paths lazily - // Look for direct paths while (requirements.length > 0) { // it's important to pop from the end as we want to process the last added requirement first const requirement = requirements.pop(); @@ -95,163 +75,138 @@ export class MoveValidator { ); if (result.success === false) { - this.cache.set(cacheKey, result); return result; } for (const innerRequirement of result.requirements) { - // at this point we should have a list of ignored tails requirements.unshift(innerRequirement); } } - this.cache.set(cacheKey, { - success: true, - errors: undefined, - }); - return { success: true, errors: undefined, }; } - private validateRequirement( - requirement: MoveRequirement, + private validateFragmentRequirement( + requirement: MoveRequirement, visitedEdges: Edge[], visitedGraphs: string[], - visitedFields: Fields[], - ): - | { - success: true; - requirements: MoveRequirement[]; - } - | { - success: false; - errors: SatisfiabilityError[]; - } { + visitedFields: Selection[], + ): RequirementResult { + this.logger.log(() => 'Validating: ... on ' + requirement.selection.typeName); + const nextPaths: OperationPath[] = []; const errors: SatisfiabilityError[] = []; - if ('type' in requirement) { - for (const path of requirement.paths) { - const directPathsResult = this.pathFinder.findDirectPaths( - path, - requirement.type.childTypeName, - null, - visitedEdges, - ); - if (directPathsResult.success) { - if (this.logger.isEnabled) { - this.logger.log(() => 'Possible direct paths:'); - for (const path of directPathsResult.paths) { - this.logger.log(() => ' ' + path.toString()); - } - } - nextPaths.push(...directPathsResult.paths); - } else { - errors.push(...directPathsResult.errors); - } - } - - // // we could add these as lazy - // try indirect paths - for (const path of requirement.paths) { - const indirectPathsResult = this.pathFinder.findIndirectPaths( - path, - requirement.type.childTypeName, - null, - visitedEdges, - visitedGraphs, - visitedFields, - ); + // Looks like we hit a fragment spread that matches the current type. + // It means that it's a fragment spread on an object type, not a union or interface. + // We can ignore the fragment and continue with the selection set. + if (requirement.paths[0].tail()?.typeName === requirement.selection.typeName) { + return { + success: true, + requirements: requirement.selection.selectionSet.map(selection => ({ + selection, + paths: requirement.paths, + })), + }; + } - if (indirectPathsResult.success) { - if (this.logger.isEnabled) { - this.logger.log(() => 'Possible indirect paths:'); - for (const path of indirectPathsResult.paths) { - this.logger.log(() => ' ' + path.toString()); - } + for (const path of requirement.paths) { + const directPathsResult = this.pathFinder.findDirectPaths( + path, + requirement.selection.typeName, + null, + visitedEdges, + ); + if (directPathsResult.success) { + if (this.logger.isEnabled) { + this.logger.log(() => 'Possible direct paths:'); + for (const path of directPathsResult.paths) { + this.logger.log(() => ' ' + path.toString()); } - nextPaths.push(...indirectPathsResult.paths); - } else { - errors.push(...indirectPathsResult.errors); } + nextPaths.push(...directPathsResult.paths); + } else { + errors.push(...directPathsResult.errors); } + } - if (nextPaths.length === 0) { + // // we could add these as lazy + // try indirect paths + for (const path of requirement.paths) { + const indirectPathsResult = this.pathFinder.findIndirectPaths( + path, + requirement.selection.typeName, + null, + visitedEdges, + visitedGraphs, + visitedFields, + ); + + if (indirectPathsResult.success) { if (this.logger.isEnabled) { - this.logger.log(() => 'Could not resolve from:'); - for (const path of requirement.paths) { + this.logger.log(() => 'Possible indirect paths:'); + for (const path of indirectPathsResult.paths) { this.logger.log(() => ' ' + path.toString()); } } - - // cannot advance - return { - success: false, - errors, - }; + nextPaths.push(...indirectPathsResult.paths); + } else { + errors.push(...indirectPathsResult.errors); } + } - if (!requirement.type.field) { - // we reached the end of the path - return { - success: true, - requirements: [], - }; + if (nextPaths.length === 0) { + if (this.logger.isEnabled) { + this.logger.log(() => 'Could not resolve from:'); + for (const path of requirement.paths) { + this.logger.log(() => ' ' + path.toString()); + } } + // cannot advance return { - success: true, - requirements: [ - { - field: requirement.type.field, - paths: nextPaths.slice(), - }, - ], + success: false, + errors, }; } - const possibleTypes = - /* KAMIL: this was originally equal to [requirement.field.typeName] - kind of */ this.supergraph.possibleTypesOf( - requirement.field.typeName, - ); - - const needsAbstractMove = !possibleTypes.includes(requirement.field.typeName); - - if (needsAbstractMove) { - const requirements: MoveRequirement[] = []; - for (const possibleType of possibleTypes) { - // we need to move to an abstract type first - const abstractMoveRequirement: MoveRequirement = { - type: { - parentTypeName: requirement.field.typeName, - childTypeName: possibleType, - field: { - ...requirement.field, - typeName: possibleType, - }, - }, - paths: requirement.paths, - }; - - requirements.push(abstractMoveRequirement); - } - - this.logger.log(() => 'Abstract move'); - + if (!requirement.selection.selectionSet || requirement.selection.selectionSet.length === 0) { + // we reached the end of the path return { success: true, - requirements, + requirements: [], }; } + return { + success: true, + requirements: requirement.selection.selectionSet.map(selection => ({ + selection, + paths: nextPaths.slice(), + })), + }; + } + + private validateFieldRequirement( + requirement: MoveRequirement, + visitedEdges: Edge[], + visitedGraphs: string[], + visitedFields: Selection[], + ): RequirementResult { + const { fieldName, typeName } = requirement.selection; + this.logger.log(() => 'Validating: ' + typeName + '.' + fieldName); + + const nextPaths: OperationPath[] = []; + const errors: SatisfiabilityError[] = []; + for (const path of requirement.paths) { const directPathsResult = this.pathFinder.findDirectPaths( path, - requirement.field.typeName, - requirement.field.fieldName, + requirement.selection.typeName, + requirement.selection.fieldName, visitedEdges, ); if (directPathsResult.success) { @@ -271,8 +226,8 @@ export class MoveValidator { for (const path of requirement.paths) { const indirectPathsResult = this.pathFinder.findIndirectPaths( path, - requirement.field.typeName, - requirement.field.fieldName, + requirement.selection.typeName, + requirement.selection.fieldName, visitedEdges, visitedGraphs, visitedFields, @@ -292,10 +247,7 @@ export class MoveValidator { } if (nextPaths.length === 0) { - this.logger.log( - () => - `Failed to resolve field ${requirement.field.typeName}.${requirement.field.fieldName} from:`, - ); + this.logger.log(() => `Failed to resolve field ${typeName}.${fieldName} from:`); if (this.logger.isEnabled) { for (const path of requirement.paths) { this.logger.log(() => ` ` + path); @@ -305,13 +257,11 @@ export class MoveValidator { // cannot advance return { success: false, - errors: errors.filter(e => - e.isMatchingField(requirement.field.typeName, requirement.field.fieldName), - ), + errors: errors.filter(e => e.isMatchingField(typeName, fieldName)), }; } - if (!requirement.field.selectionSet || requirement.field.selectionSet.length === 0) { + if (!requirement.selection.selectionSet || requirement.selection.selectionSet.length === 0) { // we reached the end of the path return { success: true, @@ -321,27 +271,46 @@ export class MoveValidator { return { success: true, - requirements: requirement.field.selectionSet.map(field => ({ - field, + requirements: requirement.selection.selectionSet.map(selection => ({ + selection, paths: nextPaths.slice(), })), }; } - isExternal(edge: Edge): boolean { - if (!isFieldEdge(edge)) { - return false; + private validateRequirement( + requirement: MoveRequirement, + visitedEdges: Edge[], + visitedGraphs: string[], + visitedFields: Selection[], + ) { + if (isFragmentRequirement(requirement)) { + return this.validateFragmentRequirement( + requirement, + visitedEdges, + visitedGraphs, + visitedFields, + ); } - if (edge.move.provided) { - return false; + if (isFieldRequirement(requirement)) { + return this.validateFieldRequirement(requirement, visitedEdges, visitedGraphs, visitedFields); } - if (!edge.head.typeState) { + throw new Error(`Unsupported requirement: ${requirement.selection.kind}`); + } + + isExternal(edge: Edge): boolean { + if (!isFieldEdge(edge)) { return false; } - if (edge.head.typeState.kind !== 'object') { + if ( + !isFieldEdge(edge) || + edge.move.provided || + !edge.head.typeState || + edge.head.typeState.kind !== 'object' + ) { return false; } @@ -352,25 +321,21 @@ export class MoveValidator { } const objectTypeStateInGraph = edge.head.typeState.byGraph.get(edge.head.graphId); - - if (!objectTypeStateInGraph) { - return false; - } - const fieldStateInGraph = fieldState.byGraph.get(edge.head.graphId); - if (!fieldStateInGraph) { + if (!fieldStateInGraph || !objectTypeStateInGraph) { return false; } - const external = fieldState.byGraph.get(edge.head.graphId)?.external ?? false; - - if (!external) { + if (!fieldStateInGraph.external) { return false; } - const isFedV1 = fieldStateInGraph.version === 'v1.0'; - if (isFedV1 && objectTypeStateInGraph.extension && fieldState.usedAsKey) { + if ( + fieldStateInGraph.version === 'v1.0' && + objectTypeStateInGraph.extension && + fieldState.usedAsKey + ) { return false; } @@ -378,31 +343,6 @@ export class MoveValidator { return true; } - // ignore if other fields in graph are external - let hasNonExternalFields = false; - if (isFedV1) { - for (const [fieldName, fieldState] of edge.head.typeState.fields) { - if (fieldName === edge.move.fieldName) { - continue; - } - - const fieldStateInGraph = fieldState.byGraph.get(edge.head.graphId); - - if (!fieldStateInGraph) { - continue; - } - - if (!fieldStateInGraph.external) { - hasNonExternalFields = true; - break; - } - } - } - - if (hasNonExternalFields) { - return false; - } - if (objectTypeStateInGraph.extension) { return false; } @@ -411,15 +351,7 @@ export class MoveValidator { } private isOverridden(edge: Edge) { - if (!isFieldEdge(edge)) { - return false; - } - - if (!edge.head.typeState) { - return false; - } - - if (!edge.head.typeState || edge.head.typeState.kind !== 'object') { + if (!isFieldEdge(edge) || !edge.head.typeState || edge.head.typeState.kind !== 'object') { return false; } @@ -447,7 +379,7 @@ export class MoveValidator { path: OperationPath, visitedEdges: Edge[], visitedGraphs: string[], - visitedFields: Fields[], + visitedFields: Selection[], ): | { success: true; @@ -487,15 +419,32 @@ export class MoveValidator { ); } + if (this.isExternal(edge)) { + this.logger.groupEnd( + () => 'Cannot move to ' + edge + ' because it is external and cross-graph', + ); + return edge.setResolvable( + false, + visitedGraphs, + SatisfiabilityError.forExternal( + edge.head.graphName, + edge.move.typeName, + edge.move.fieldName, + ), + ); + } + if (edge.move.requires) { this.logger.log(() => 'Detected @requires'); - const newVisitedGraphs = concatIfNotExistsString(visitedGraphs, edge.tail.graphName); + const newVisitedGraphs = edge.isCrossGraphEdge() + ? concatIfNotExistsString(visitedGraphs, edge.tail.graphName) + : visitedGraphs; const newVisitedFields = concatIfNotExistsFields(visitedFields, edge.move.requires); this.logger.log(() => 'Visited graphs: ' + newVisitedGraphs.join(',')); if ( - this.canResolveFields( - edge.move.requires.fields, + this.canResolveSelectionSet( + edge.move.requires.selectionSet, path, visitedEdges.concat(edge), newVisitedGraphs, @@ -518,56 +467,51 @@ export class MoveValidator { edge.move.fieldName, ), }; - } else if (this.isExternal(edge)) { - this.logger.groupEnd( - () => 'Cannot move to ' + edge + ' because it is external and cross-graph', - ); - return edge.setResolvable( - false, - visitedGraphs, - SatisfiabilityError.forExternal( - edge.head.graphName, - edge.move.typeName, - edge.move.fieldName, - ), - ); - } - } else if (isEntityEdge(edge)) { - this.logger.log(() => 'Detected @key'); - const newVisitedGraphs = concatIfNotExistsString(visitedGraphs, edge.tail.graphName); - const newVisitedFields = concatIfNotExistsFields(visitedFields, edge.move.keyFields); - this.logger.log(() => 'Visited graphs: ' + newVisitedGraphs.join(',')); - if ( - this.canResolveFields( - edge.move.keyFields.fields, - path, - visitedEdges.concat(edge), - newVisitedGraphs, - newVisitedFields, - ).success - ) { - this.logger.groupEnd(() => 'Can move to ' + edge); - return edge.setResolvable(true, newVisitedGraphs); } - this.logger.groupEnd( - () => 'Cannot move to ' + edge + ' because key fields are not resolvable', - ); + this.logger.groupEnd(() => 'Can move to ' + edge); + return edge.setResolvable(true, visitedGraphs); + } - return edge.setResolvable( - false, - newVisitedGraphs, - SatisfiabilityError.forKey( - edge.head.graphName, - edge.tail.graphName, - edge.head.typeName, - edge.move.keyFields.toString(), - ), - ); + if (!isEntityEdge(edge) && !isAbstractEdge(edge)) { + throw new Error('Expected edge to be entity or abstract'); + } + + if (!edge.move.keyFields) { + this.logger.groupEnd(() => 'Can move to ' + edge); + return edge.setResolvable(true, visitedGraphs); } - this.logger.groupEnd(() => 'Can move to ' + edge); + const newVisitedGraphs = concatIfNotExistsString(visitedGraphs, edge.tail.graphName); + const newVisitedFields = concatIfNotExistsFields(visitedFields, edge.move.keyFields); + const keyFields = edge.move.keyFields; - return edge.setResolvable(true, visitedGraphs); + this.logger.log(() => 'Detected @key'); + this.logger.log(() => 'Visited graphs: ' + newVisitedGraphs.join(',')); + const resolvable = this.canResolveSelectionSet( + keyFields.selectionSet, + path, + visitedEdges.concat(edge), + newVisitedGraphs, + newVisitedFields, + ).success; + + if (resolvable) { + this.logger.groupEnd(() => 'Can move to ' + edge); + return edge.setResolvable(true, newVisitedGraphs); + } + + this.logger.groupEnd(() => 'Cannot move to ' + edge + ' because key fields are not resolvable'); + + return edge.setResolvable( + false, + newVisitedGraphs, + SatisfiabilityError.forKey( + edge.head.graphName, + edge.tail.graphName, + edge.head.typeName, + keyFields.toString(), + ), + ); } } diff --git a/src/supergraph/validation/rules/satisfiablity/moves.ts b/src/supergraph/validation/rules/satisfiablity/moves.ts index ae740ce..42d3b8e 100644 --- a/src/supergraph/validation/rules/satisfiablity/moves.ts +++ b/src/supergraph/validation/rules/satisfiablity/moves.ts @@ -1,5 +1,5 @@ -import type { Fields } from './fields.js'; import { lazy } from './helpers.js'; +import type { Selection } from './selection.js'; export interface Move { toString(): string; @@ -27,8 +27,8 @@ export class FieldMove implements Move { constructor( public typeName: string, public fieldName: string, - public requires: Fields | null = null, - public provides: Fields | null = null, + public requires: Selection | null = null, + public provides: Selection | null = null, public provided: boolean = false, ) {} @@ -38,15 +38,19 @@ export class FieldMove implements Move { } export class AbstractMove implements Move { + private _toString = lazy(() => (this.keyFields ? `🔮 🔑 ${this.keyFields}` : `🔮`)); + + constructor(public keyFields?: Selection) {} + toString() { - return `🔮`; + return this._toString.get(); } } export class EntityMove implements Move { private _toString = lazy(() => `🔑 ${this.keyFields}`); - constructor(public keyFields: Fields) {} + constructor(public keyFields: Selection) {} toString() { return this._toString.get(); diff --git a/src/supergraph/validation/rules/satisfiablity/operation-path.ts b/src/supergraph/validation/rules/satisfiablity/operation-path.ts index 48f8dc3..5d65706 100644 --- a/src/supergraph/validation/rules/satisfiablity/operation-path.ts +++ b/src/supergraph/validation/rules/satisfiablity/operation-path.ts @@ -1,6 +1,5 @@ import { isFieldEdge, type Edge } from './edge.js'; import { lazy } from './helpers.js'; -import { FieldMove } from './moves.js'; import type { Node } from './node.js'; export type Step = FieldStep | AbstractStep; diff --git a/src/supergraph/validation/rules/satisfiablity/fields.ts b/src/supergraph/validation/rules/satisfiablity/selection.ts similarity index 50% rename from src/supergraph/validation/rules/satisfiablity/fields.ts rename to src/supergraph/validation/rules/satisfiablity/selection.ts index 662d976..f3c912d 100644 --- a/src/supergraph/validation/rules/satisfiablity/fields.ts +++ b/src/supergraph/validation/rules/satisfiablity/selection.ts @@ -3,30 +3,33 @@ import { parseFields } from '../../../../subgraph/helpers.js'; import { stripTypeModifiers } from '../../../../utils/state.js'; import { SupergraphState } from '../../../state.js'; -export type Field = - | { - typeName: string; - fieldName: string; - selectionSet: null; - } - | { - typeName: string; - fieldName: string; - selectionSet: Array; - }; - -export class Fields { +export type Field = { + kind: 'field'; + typeName: string; + fieldName: string; + selectionSet: null | Array; +}; + +export type Fragment = { + kind: 'fragment'; + typeName: string; + selectionSet: Array; +}; + +export type SelectionNode = Field | Fragment; + +export class Selection { constructor( private typeName: string, private source: string, - public fields: Field[], + public selectionSet: SelectionNode[], ) {} contains(typeName: string, fieldName: string) { - return this._contains(typeName, fieldName, this.fields); + return this._contains(typeName, fieldName, this.selectionSet); } - equals(other: Fields) { + equals(other: Selection) { if (this.typeName !== other.typeName) { return false; } @@ -35,31 +38,46 @@ export class Fields { return true; } - return this._fieldsEqual(this.fields, other.fields); + return this._selectionSetEqual(this.selectionSet, other.selectionSet); } - private _fieldsEqual(fields: Field[], otherFields: Field[]): boolean { - if (fields.length !== otherFields.length) { + private _selectionSetEqual( + selectionSet: SelectionNode[], + otherSelectionSet: SelectionNode[], + ): boolean { + if (selectionSet.length !== otherSelectionSet.length) { return false; } - for (let i = 0; i < fields.length; i++) { + for (let i = 0; i < selectionSet.length; i++) { // Fields are sorted by typeName and fieldName, so we can compare them directly. - // See: FieldsResolver#sortFields - const field = fields[i]; - const otherField = otherFields[i]; + // See: SelectionResolver#sort + const selectionNode = selectionSet[i]; + const otherSelectionNode = otherSelectionSet[i]; + + if (selectionNode.kind !== otherSelectionNode.kind) { + return false; + } // Compare typeName and fieldName - if (field.typeName !== otherField.typeName || field.fieldName !== otherField.fieldName) { + if (selectionNode.typeName !== otherSelectionNode.typeName) { + return false; + } + + if ( + selectionNode.kind === 'field' && + otherSelectionNode.kind === 'field' && + selectionNode.fieldName !== otherSelectionNode.fieldName + ) { return false; } const areEqual = // Compare selectionSet if both are arrays // Otherwise, compare nullability of selectionSet - Array.isArray(field.selectionSet) && Array.isArray(otherField.selectionSet) - ? this._fieldsEqual(field.selectionSet, otherField.selectionSet) - : field.selectionSet === otherField.selectionSet; + Array.isArray(selectionNode.selectionSet) && Array.isArray(otherSelectionNode.selectionSet) + ? this._selectionSetEqual(selectionNode.selectionSet, otherSelectionNode.selectionSet) + : selectionNode.selectionSet === otherSelectionNode.selectionSet; // Avoid unnecessary iterations if we already know that fields are not equal if (!areEqual) { @@ -70,35 +88,37 @@ export class Fields { return true; } - private _contains(typeName: string, fieldName: string, fields: Field[]): boolean { - return fields.some( - f => - (f.typeName === typeName && f.fieldName === fieldName) || - (f.selectionSet ? this._contains(typeName, fieldName, f.selectionSet) : false), + private _contains(typeName: string, fieldName: string, selectionSet: SelectionNode[]): boolean { + return selectionSet.some( + s => + (s.kind === 'field' && s.typeName === typeName && s.fieldName === fieldName) || + (s.selectionSet ? this._contains(typeName, fieldName, s.selectionSet) : false), ); } toString() { - return this.source; + return this.source.replace(/\s+/g, ' '); } } -export class FieldsResolver { - private cache: Map = new Map(); +export class SelectionResolver { + private cache: Map = new Map(); constructor(private supergraphState: SupergraphState) {} - resolve(typeName: string, keyFields: string): Fields { + resolve(typeName: string, keyFields: string): Selection { const key = this.keyFactory(typeName, keyFields); if (this.cache.has(key)) { return this.cache.get(key)!; } - const typeState = this.supergraphState.objectTypes.get(typeName); + const typeState = + this.supergraphState.objectTypes.get(typeName) ?? + this.supergraphState.interfaceTypes.get(typeName); if (!typeState) { - throw new Error(`Expected an object type when resolving keyFields of ${typeName}`); + throw new Error(`Expected an object/interface type when resolving keyFields of ${typeName}`); } const selectionSetNode = parseFields(keyFields); @@ -107,7 +127,7 @@ export class FieldsResolver { throw new Error(`Expected a selection set when resolving keyFields of ${typeName}`); } - const fields = new Fields( + const fields = new Selection( typeName, keyFields, this.resolveSelectionSetNode(typeName, selectionSetNode), @@ -121,7 +141,11 @@ export class FieldsResolver { return `${typeName}/${keyFields}`; } - private resolveFieldNode(typeName: string, fieldNode: FieldNode, fields: Field[]) { + private resolveFieldNode(typeName: string, fieldNode: FieldNode, selectionSet: SelectionNode[]) { + if (fieldNode.name.value === '__typename') { + return; + } + const typeState = this.supergraphState.objectTypes.get(typeName) ?? this.supergraphState.interfaceTypes.get(typeName); @@ -130,10 +154,6 @@ export class FieldsResolver { throw new Error(`Type "${typeName}" is not defined.`); } - if (fieldNode.name.value === '__typename') { - return; - } - if (!typeState.fields.has(fieldNode.name.value)) { throw new Error( `Type "${typeName.toString()}" does not have field "${fieldNode.name.value}".`, @@ -143,14 +163,16 @@ export class FieldsResolver { if (fieldNode.selectionSet) { const outputType = stripTypeModifiers(typeState.fields.get(fieldNode.name.value)!.type); - fields.push({ + selectionSet.push({ + kind: 'field', fieldName: fieldNode.name.value, typeName, selectionSet: this.resolveSelectionSetNode(outputType, fieldNode.selectionSet), }); } else { // it's a leaf - fields.push({ + selectionSet.push({ + kind: 'field', typeName, fieldName: fieldNode.name.value, selectionSet: null, @@ -158,7 +180,10 @@ export class FieldsResolver { } } - private resolveInlineFragmentNode(fragmentNode: InlineFragmentNode, fields: Field[]) { + private resolveInlineFragmentNode( + fragmentNode: InlineFragmentNode, + selectionSet: SelectionNode[], + ) { if (!fragmentNode.typeCondition?.name.value) { throw new Error(`Inline fragment without type condition is not supported.`); } @@ -173,36 +198,43 @@ export class FieldsResolver { throw new Error(`Type "${typeName}" is not defined.`); } - for (const selection of fragmentNode.selectionSet.selections) { - if (selection.kind === Kind.FIELD) { - this.resolveFieldNode(typeName, selection, fields); - } else { - throw new Error(`Inline fragment within an inline fragment is not supported.`); - } - } + selectionSet.push({ + kind: 'fragment', + typeName, + selectionSet: this.resolveSelectionSetNode(typeName, fragmentNode.selectionSet), + }); } private resolveSelectionSetNode( typeName: string, selectionSetNode: SelectionSetNode, - fields: Field[] = [], - ): Field[] { + selectionSet: SelectionNode[] = [], + ): SelectionNode[] { for (const selection of selectionSetNode.selections) { if (selection.kind === Kind.FIELD) { - this.resolveFieldNode(typeName, selection, fields); + this.resolveFieldNode(typeName, selection, selectionSet); } else if (selection.kind === Kind.INLINE_FRAGMENT) { - this.resolveInlineFragmentNode(selection, fields); + this.resolveInlineFragmentNode(selection, selectionSet); } else { throw new Error(`Fragment spread is not supported.`); } } - return this.sortFields(fields); + return this.sort(selectionSet); } - private sortFields(fields: Field[]) { - return fields.sort((a, b) => - `${a.typeName}.${a.fieldName}`.localeCompare(`${b.typeName}.${b.fieldName}`), - ); + private sort(selectionSet: SelectionNode[]): SelectionNode[] { + return selectionSet.sort((a, b) => { + if (a.kind === b.kind) { + return a.kind === 'field' && b.kind === 'field' + ? // sort fields by typeName.fieldName + `${a.typeName}.${a.fieldName}`.localeCompare(`${b.typeName}.${b.fieldName}`) + : // sort fragments by typeName + a.typeName.localeCompare(b.typeName); + } + + // field -> fragment + return a.kind === 'field' ? -1 : 1; + }); } } diff --git a/src/supergraph/validation/rules/satisfiablity/supergraph.ts b/src/supergraph/validation/rules/satisfiablity/supergraph.ts index be313f5..b69c80f 100644 --- a/src/supergraph/validation/rules/satisfiablity/supergraph.ts +++ b/src/supergraph/validation/rules/satisfiablity/supergraph.ts @@ -2,27 +2,27 @@ import { OperationTypeNode } from 'graphql'; import { Logger, LoggerContext } from '../../../../utils/logger.js'; import type { SupergraphState } from '../../../state.js'; import { SUPERGRAPH_ID } from './constants.js'; -import { FieldsResolver } from './fields.js'; import { Graph } from './graph.js'; import { MoveValidator } from './move-validator.js'; -import { Step } from './operation-path.js'; +import type { Step } from './operation-path.js'; +import { SelectionResolver } from './selection.js'; import { Walker } from './walker.js'; export class Supergraph { private supergraph: Graph; private mergedGraph: Graph; - private fieldsResolver: FieldsResolver; + private selectionResolver: SelectionResolver; private moveRequirementChecker: MoveValidator; private logger = new Logger('Supergraph', new LoggerContext()); constructor(supergraphState: SupergraphState) { - this.fieldsResolver = new FieldsResolver(supergraphState); + this.selectionResolver = new SelectionResolver(supergraphState); this.supergraph = new Graph( this.logger, SUPERGRAPH_ID, 'supergraph', supergraphState, - this.fieldsResolver, + this.selectionResolver, true, ); this.mergedGraph = new Graph( @@ -30,7 +30,7 @@ export class Supergraph { SUPERGRAPH_ID, 'merged', supergraphState, - this.fieldsResolver, + this.selectionResolver, ); for (const [id, subgraphState] of supergraphState.subgraphs) { this.mergedGraph.addSubgraph( @@ -39,7 +39,7 @@ export class Supergraph { id, subgraphState.graph.name, supergraphState, - this.fieldsResolver, + this.selectionResolver, false, ) .addFromRoots() @@ -49,7 +49,8 @@ export class Supergraph { } this.mergedGraph.joinSubgraphs(); - this.supergraph.addFromRoots(); + + this.supergraph.addFromRoots().addInterfaceObjectFields(); this.moveRequirementChecker = new MoveValidator(this.logger, this.mergedGraph); } diff --git a/src/supergraph/validation/rules/satisfiablity/walker.ts b/src/supergraph/validation/rules/satisfiablity/walker.ts index 62c77fd..7922ee7 100644 --- a/src/supergraph/validation/rules/satisfiablity/walker.ts +++ b/src/supergraph/validation/rules/satisfiablity/walker.ts @@ -362,11 +362,13 @@ export class Walker { const unreachable: WalkTracker[] = []; const queue: WalkTracker[] = []; - const rootNodes = ['Query', 'Mutation', 'Subscription'] - .map(name => this.supergraph.nodeOf(name, false)) - .filter((node): node is Node => !!node); + for (const name of ['Query', 'Mutation', 'Subscription']) { + const rootNode = this.supergraph.nodeOf(name, false); + + if (!rootNode) { + continue; + } - for (const rootNode of rootNodes) { queue.push( new WalkTracker( new OperationPath(rootNode), diff --git a/src/supergraph/validation/rules/types-of-the-same-kind-rule.ts b/src/supergraph/validation/rules/types-of-the-same-kind-rule.ts index 16b32e2..cf5d965 100644 --- a/src/supergraph/validation/rules/types-of-the-same-kind-rule.ts +++ b/src/supergraph/validation/rules/types-of-the-same-kind-rule.ts @@ -1,9 +1,5 @@ import { GraphQLError } from 'graphql'; -import { ObjectType, TypeKind } from '../../../subgraph/state.js'; -import { - allowedInterfaceObjectVersion, - importsAllowInterfaceObject, -} from '../../../subgraph/validation/rules/elements/interface-object.js'; +import { TypeKind } from '../../../subgraph/state.js'; import { SupergraphValidationContext } from './../validation-context.js'; const mapIRKindToString = { @@ -18,7 +14,7 @@ const mapIRKindToString = { export type GraphTypeValidationContext = { graphName: string; - interfaceObject: boolean; + isInterfaceObject: boolean; }; export function TypesOfTheSameKindRule(context: SupergraphValidationContext) { @@ -30,12 +26,13 @@ export function TypesOfTheSameKindRule(context: SupergraphValidationContext) { for (const [graph, state] of context.subgraphStates) { state.types.forEach(type => { - let interfaceObject = false; const kindToGraphs = typeToKindWithGraphs.get(type.name); - const typeIsObject = type.kind === TypeKind.OBJECT; - if (typeIsObject && type.interfaceObjectTypeName) { - interfaceObject = true; - } + const isInterfaceObject = type.kind === TypeKind.INTERFACE ? type.isInterfaceObject : false; + + const graphsValue = { + graphName: context.graphIdToName(graph), + isInterfaceObject, + }; if (kindToGraphs) { // Seems like we've already seen this type @@ -44,21 +41,10 @@ export function TypesOfTheSameKindRule(context: SupergraphValidationContext) { if (graphs) { // If we've already seen this kind // Add the graph to the set. - graphs.add({ - graphName: context.graphIdToName(graph), - interfaceObject, - }); + graphs.add(graphsValue); } else { // Add the kind to the map of kinds for that type - kindToGraphs.set( - type.kind, - new Set([ - { - graphName: context.graphIdToName(graph), - interfaceObject, - }, - ]), - ); + kindToGraphs.set(type.kind, new Set([graphsValue])); } // If it has more than 1 kind @@ -68,20 +54,7 @@ export function TypesOfTheSameKindRule(context: SupergraphValidationContext) { } } else { // We haven't seen this type yet - typeToKindWithGraphs.set( - type.name, - new Map([ - [ - type.kind, - new Set([ - { - graphName: context.graphIdToName(graph), - interfaceObject, - }, - ]), - ], - ]), - ); + typeToKindWithGraphs.set(type.name, new Map([[type.kind, new Set([graphsValue])]])); } }); } @@ -89,9 +62,7 @@ export function TypesOfTheSameKindRule(context: SupergraphValidationContext) { for (const typeName of typesWithConflict) { const kindToGraphs = typeToKindWithGraphs.get(typeName)!; - // check for @interfaceObject (trkohler) - const isInterfaceObjectCandidate = interfaceObjectConditions(kindToGraphs); - if (isInterfaceObjectCandidate) { + if (interfaceObjectConditions(kindToGraphs)) { continue; } @@ -121,12 +92,11 @@ export function TypesOfTheSameKindRule(context: SupergraphValidationContext) { function interfaceObjectConditions( kindToGraphs: Map>, ): boolean { - const objectTypes = kindToGraphs.get(TypeKind.OBJECT) || []; - let interfaceObject = false; - for (const graphTypeValidationContext of objectTypes) { - if (graphTypeValidationContext.interfaceObject) { - interfaceObject = true; + const interfaceTypes = kindToGraphs.get(TypeKind.INTERFACE) || []; + for (const graphTypeValidationContext of interfaceTypes) { + if (graphTypeValidationContext.isInterfaceObject) { + return true; } } - return interfaceObject; + return false; } diff --git a/src/supergraph/validation/validate-supergraph.ts b/src/supergraph/validation/validate-supergraph.ts index 44a7fbe..19d9921 100644 --- a/src/supergraph/validation/validate-supergraph.ts +++ b/src/supergraph/validation/validate-supergraph.ts @@ -14,7 +14,7 @@ import { FieldsOfTheSameTypeRule } from './rules/fields-of-the-same-type-rule.js import { InputFieldDefaultMismatchRule } from './rules/input-field-default-mismatch-rule.js'; import { InputObjectValuesRule } from './rules/input-object-values-rule.js'; import { InterfaceKeyMissingImplementationTypeRule } from './rules/interface-key-missing-implementation-type.js'; -import { InterfaceObjectCompositionRule } from "./rules/interface-object-composition-rules.js"; +import { InterfaceObjectUsageErrorRule } from './rules/interface-object-usage-error.js'; import { InvalidFieldSharingRule } from './rules/invalid-field-sharing-rule.js'; import { OnlyInaccessibleChildrenRule } from './rules/only-inaccessible-children-rule.js'; import { OverrideSourceHasOverrideRule } from './rules/override-source-has-override.js'; @@ -40,8 +40,7 @@ export function validateSupergraph( for (const subgraphState of subgraphStates.values()) { state.addSubgraph(subgraphState); } - // I need to define interfaceObject rule there because it does modifications to fields (trkohler) - const preSupergraphRules = [RequiredQueryRule, TypesOfTheSameKindRule, InterfaceObjectCompositionRule]; + const preSupergraphRules = [RequiredQueryRule, TypesOfTheSameKindRule]; const rulesToSkip = __internal?.disableValidationRules ?? []; for (const rule of preSupergraphRules) { @@ -72,6 +71,7 @@ export function validateSupergraph( OnlyInaccessibleChildrenRule, ReferencedInaccessibleRule, DirectiveCompositionRule, + InterfaceObjectUsageErrorRule, InterfaceKeyMissingImplementationTypeRule, ExternalTypeMismatchRule, InvalidFieldSharingRule, diff --git a/src/utils/version.ts b/src/utils/version.ts new file mode 100644 index 0000000..c71714f --- /dev/null +++ b/src/utils/version.ts @@ -0,0 +1,20 @@ +import { FederationVersion } from '../specifications/federation'; + +export function satisfiesVersionRange( + version: FederationVersion, + range: `${'<' | '>=' | '>'} ${FederationVersion}`, +) { + const [sign, ver] = range.split(' ') as ['<' | '>=' | '>', FederationVersion]; + const versionInRange = parseFloat(ver.replace('v', '')); + const detectedVersion = parseFloat(version.replace('v', '')); + + if (sign === '<') { + return detectedVersion < versionInRange; + } + + if (sign === '>') { + return detectedVersion > versionInRange; + } + + return detectedVersion >= versionInRange; +} diff --git a/src/validate.ts b/src/validate.ts index 7de97b4..0b26b18 100644 --- a/src/validate.ts +++ b/src/validate.ts @@ -139,7 +139,6 @@ export function validateSubgraph(subgraph: { name: string; url?: string; typeDef graph, graph.typeDefs, detectedFederationSpec.get(graph.id)!.version, - detectedFederationSpec.get(graph.id)!.imports, corePerSubgraph[i].links ?? [], ), ]), @@ -198,7 +197,6 @@ export function validate( graph, graph.typeDefs, detectedFederationSpec.get(graph.id)!.version, - detectedFederationSpec.get(graph.id)!.imports, corePerSubgraph[i].links ?? [], ), ]),