mirror of
https://github.com/graphql-hive/console
synced 2026-04-21 14:37:17 +00:00
fix: use schema contracts federation library (#6964)
This commit is contained in:
parent
2d7e7be313
commit
147e86233c
18 changed files with 64 additions and 4151 deletions
|
|
@ -21,7 +21,7 @@
|
|||
"@hive/schema": "workspace:*",
|
||||
"@hive/server": "workspace:*",
|
||||
"@hive/storage": "workspace:*",
|
||||
"@theguild/federation-composition": "0.19.1",
|
||||
"@theguild/federation-composition": "0.20.0",
|
||||
"@trpc/client": "10.45.2",
|
||||
"@trpc/server": "10.45.2",
|
||||
"@types/async-retry": "1.4.8",
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@
|
|||
"@sentry/cli": "2.40.0",
|
||||
"@swc/core": "1.13.5",
|
||||
"@theguild/eslint-config": "0.12.1",
|
||||
"@theguild/federation-composition": "0.20.0",
|
||||
"@theguild/prettier-config": "2.0.7",
|
||||
"@types/node": "22.10.5",
|
||||
"bob-the-bundler": "7.0.1",
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@
|
|||
"@oclif/core": "^3.26.6",
|
||||
"@oclif/plugin-help": "6.0.22",
|
||||
"@oclif/plugin-update": "4.2.13",
|
||||
"@theguild/federation-composition": "0.19.1",
|
||||
"@theguild/federation-composition": "0.20.0",
|
||||
"colors": "1.4.0",
|
||||
"env-ci": "7.3.0",
|
||||
"graphql": "^16.8.1",
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
"@sentry/node": "7.120.2",
|
||||
"@sentry/types": "7.120.2",
|
||||
"@slack/web-api": "7.8.0",
|
||||
"@theguild/federation-composition": "0.19.1",
|
||||
"@theguild/federation-composition": "0.20.0",
|
||||
"@trpc/client": "10.45.2",
|
||||
"@trpc/server": "10.45.2",
|
||||
"@types/bcryptjs": "2.4.6",
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ export default gql`
|
|||
description: String @tag(name: "public")
|
||||
"""
|
||||
List of permissions that are assigned to the access token.
|
||||
A list of available permissions can be retrieved via the \`Organization.availableOrganizationAccessTokenPermissionGroups\` field.
|
||||
A list of available permissions can be retrieved via the 'Organization.availableOrganizationAccessTokenPermissionGroups' field.
|
||||
"""
|
||||
permissions: [String!]! @tag(name: "public")
|
||||
"""
|
||||
|
|
@ -521,7 +521,7 @@ export default gql`
|
|||
"""
|
||||
description: String! @tag(name: "public")
|
||||
"""
|
||||
A list of available permissions can be retrieved via the \`Organization.availableMemberPermissionGroups\` field.
|
||||
A list of available permissions can be retrieved via the 'Organization.availableMemberPermissionGroups' field.
|
||||
"""
|
||||
selectedPermissions: [String!]! @tag(name: "public")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@ import { gql } from 'graphql-modules';
|
|||
|
||||
export default gql`
|
||||
"""
|
||||
A date-time string at UTC, such as \`2007-12-03T10:15:30Z\`, is compliant with the date-time format outlined
|
||||
A date-time string at UTC, such as '2007-12-03T10:15:30Z', is compliant with the date-time format outlined
|
||||
in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar.
|
||||
|
||||
This scalar is a description of an exact instant on the timeline such as the instant that a user account was created.
|
||||
|
||||
This scalar ignores leap seconds (thereby assuming that a minute constitutes 59 seconds). In this respect, it diverges from the RFC 3339 profile.
|
||||
|
||||
Where an RFC 3339 compliant date-time string has a time-zone other than UTC, it is shifted to UTC. For example, the date-time string \`2016-01-01T14:10:20+01:00\` is shifted to \`2016-01-01T13:10:20Z\`.
|
||||
Where an RFC 3339 compliant date-time string has a time-zone other than UTC, it is shifted to UTC. For example, the date-time string '2016-01-01T14:10:20+01:00' is shifted to '2016-01-01T13:10:20Z'.
|
||||
"""
|
||||
scalar DateTime
|
||||
@tag(name: "public")
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
"@graphql-tools/stitching-directives": "3.1.38",
|
||||
"@hive/service-common": "workspace:*",
|
||||
"@sentry/node": "7.120.2",
|
||||
"@theguild/federation-composition": "0.19.1",
|
||||
"@theguild/federation-composition": "0.20.0",
|
||||
"@trpc/server": "10.45.2",
|
||||
"@types/async-retry": "1.4.8",
|
||||
"@types/ioredis-mock": "8.2.5",
|
||||
|
|
|
|||
|
|
@ -1,596 +0,0 @@
|
|||
import { createComposeFederation } from '../federation';
|
||||
|
||||
test('contract: mutation type is not part of the public schema if all fields are excluded', async () => {
|
||||
const compose = createComposeFederation({
|
||||
decrypt: () => '',
|
||||
requestTimeoutMs: Infinity,
|
||||
});
|
||||
|
||||
const sdl = /* GraphQL */ `
|
||||
schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@tag"]) {
|
||||
query: Query
|
||||
mutation: Mutation
|
||||
}
|
||||
|
||||
type Query {
|
||||
field1: String!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
field1: ID! @tag(name: "exclude")
|
||||
}
|
||||
`;
|
||||
|
||||
const sdl2 = /* GraphQL */ `
|
||||
schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@tag"]) {
|
||||
query: Query
|
||||
mutation: Mutation
|
||||
}
|
||||
|
||||
type Query {
|
||||
field2: String!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
field2: ID! @tag(name: "exclude")
|
||||
}
|
||||
`;
|
||||
|
||||
const result = await compose({
|
||||
contracts: [
|
||||
{
|
||||
filter: {
|
||||
include: null,
|
||||
exclude: ['exclude'],
|
||||
removeUnreachableTypesFromPublicApiSchema: true,
|
||||
},
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
external: null,
|
||||
native: true,
|
||||
requestId: '1',
|
||||
schemas: [
|
||||
{
|
||||
raw: sdl,
|
||||
source: 'foo.graphql',
|
||||
url: 'https://lol.de',
|
||||
},
|
||||
{
|
||||
raw: sdl2,
|
||||
source: 'foo2.graphql',
|
||||
url: 'https://trolol.de',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.type).toEqual('success');
|
||||
const contractResult = result.result.contracts?.at(0);
|
||||
expect(contractResult?.id).toEqual('1');
|
||||
expect(contractResult?.result.type).toEqual('success');
|
||||
expect(contractResult?.result.result.sdl).toMatchInlineSnapshot(`
|
||||
type Query {
|
||||
field1: String!
|
||||
field2: String!
|
||||
}
|
||||
`);
|
||||
expect(contractResult?.result.result.supergraph).toMatchInlineSnapshot(`
|
||||
schema @link(url: "https://specs.apollo.dev/link/v1.0") @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY) {
|
||||
query: Query
|
||||
mutation: Mutation
|
||||
}
|
||||
|
||||
directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
|
||||
|
||||
directive @join__graph(name: String!, url: String!) on ENUM_VALUE
|
||||
|
||||
directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
|
||||
|
||||
directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE
|
||||
|
||||
directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
|
||||
|
||||
directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
|
||||
|
||||
scalar join__FieldSet
|
||||
|
||||
directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
|
||||
|
||||
scalar link__Import
|
||||
|
||||
enum link__Purpose {
|
||||
"""
|
||||
\`SECURITY\` features provide metadata necessary to securely resolve fields.
|
||||
"""
|
||||
SECURITY
|
||||
"""
|
||||
\`EXECUTION\` features provide metadata necessary for operation execution.
|
||||
"""
|
||||
EXECUTION
|
||||
}
|
||||
|
||||
directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ENUM | ENUM_VALUE | SCALAR | INPUT_OBJECT | INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION
|
||||
|
||||
enum join__Graph {
|
||||
FOO_GRAPHQL @join__graph(name: "foo.graphql", url: "https://lol.de")
|
||||
FOO2_GRAPHQL @join__graph(name: "foo2.graphql", url: "https://trolol.de")
|
||||
}
|
||||
|
||||
type Query @join__type(graph: FOO_GRAPHQL) @join__type(graph: FOO2_GRAPHQL) {
|
||||
field1: String! @join__field(graph: FOO_GRAPHQL)
|
||||
field2: String! @join__field(graph: FOO2_GRAPHQL)
|
||||
}
|
||||
|
||||
type Mutation @join__type(graph: FOO_GRAPHQL) @join__type(graph: FOO2_GRAPHQL) @inaccessible {
|
||||
field1: ID! @join__field(graph: FOO_GRAPHQL) @inaccessible
|
||||
field2: ID! @join__field(graph: FOO2_GRAPHQL) @inaccessible
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('contract: mutation type is part of the public schema if not all fields are excluded', async () => {
|
||||
const compose = createComposeFederation({
|
||||
decrypt: () => '',
|
||||
requestTimeoutMs: Infinity,
|
||||
});
|
||||
|
||||
const sdl = /* GraphQL */ `
|
||||
schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@tag"]) {
|
||||
query: Query
|
||||
mutation: Mutation
|
||||
}
|
||||
|
||||
type Query {
|
||||
field1: String!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
field1: ID! @tag(name: "exclude")
|
||||
}
|
||||
`;
|
||||
|
||||
const sdl2 = /* GraphQL */ `
|
||||
schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@tag"]) {
|
||||
query: Query
|
||||
mutation: Mutation
|
||||
}
|
||||
|
||||
type Query {
|
||||
field2: String!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
field2: ID!
|
||||
}
|
||||
`;
|
||||
|
||||
const result = await compose({
|
||||
contracts: [
|
||||
{
|
||||
filter: {
|
||||
include: null,
|
||||
exclude: ['exclude'],
|
||||
removeUnreachableTypesFromPublicApiSchema: true,
|
||||
},
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
external: null,
|
||||
native: true,
|
||||
requestId: '1',
|
||||
schemas: [
|
||||
{
|
||||
raw: sdl,
|
||||
source: 'foo.graphql',
|
||||
url: 'https://lol.de',
|
||||
},
|
||||
{
|
||||
raw: sdl2,
|
||||
source: 'foo2.graphql',
|
||||
url: 'https://trolol.de',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.type).toEqual('success');
|
||||
const contractResult = result.result.contracts?.at(0);
|
||||
expect(contractResult?.id).toEqual('1');
|
||||
expect(contractResult?.result.type).toEqual('success');
|
||||
expect(contractResult?.result.result.sdl).toMatchInlineSnapshot(`
|
||||
type Query {
|
||||
field1: String!
|
||||
field2: String!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
field2: ID!
|
||||
}
|
||||
`);
|
||||
expect(contractResult?.result.result.supergraph).toMatchInlineSnapshot(`
|
||||
schema @link(url: "https://specs.apollo.dev/link/v1.0") @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY) {
|
||||
query: Query
|
||||
mutation: Mutation
|
||||
}
|
||||
|
||||
directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
|
||||
|
||||
directive @join__graph(name: String!, url: String!) on ENUM_VALUE
|
||||
|
||||
directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
|
||||
|
||||
directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE
|
||||
|
||||
directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
|
||||
|
||||
directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
|
||||
|
||||
scalar join__FieldSet
|
||||
|
||||
directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
|
||||
|
||||
scalar link__Import
|
||||
|
||||
enum link__Purpose {
|
||||
"""
|
||||
\`SECURITY\` features provide metadata necessary to securely resolve fields.
|
||||
"""
|
||||
SECURITY
|
||||
"""
|
||||
\`EXECUTION\` features provide metadata necessary for operation execution.
|
||||
"""
|
||||
EXECUTION
|
||||
}
|
||||
|
||||
directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ENUM | ENUM_VALUE | SCALAR | INPUT_OBJECT | INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION
|
||||
|
||||
enum join__Graph {
|
||||
FOO_GRAPHQL @join__graph(name: "foo.graphql", url: "https://lol.de")
|
||||
FOO2_GRAPHQL @join__graph(name: "foo2.graphql", url: "https://trolol.de")
|
||||
}
|
||||
|
||||
type Query @join__type(graph: FOO_GRAPHQL) @join__type(graph: FOO2_GRAPHQL) {
|
||||
field1: String! @join__field(graph: FOO_GRAPHQL)
|
||||
field2: String! @join__field(graph: FOO2_GRAPHQL)
|
||||
}
|
||||
|
||||
type Mutation @join__type(graph: FOO_GRAPHQL) @join__type(graph: FOO2_GRAPHQL) {
|
||||
field1: ID! @join__field(graph: FOO_GRAPHQL) @inaccessible
|
||||
field2: ID! @join__field(graph: FOO2_GRAPHQL)
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('contract: mutation type is not part of the public schema if no fields are included', async () => {
|
||||
const compose = createComposeFederation({
|
||||
decrypt: () => '',
|
||||
requestTimeoutMs: Infinity,
|
||||
});
|
||||
|
||||
const sdl = /* GraphQL */ `
|
||||
schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@tag"]) {
|
||||
query: Query
|
||||
mutation: Mutation
|
||||
}
|
||||
|
||||
type Query {
|
||||
field1: String! @tag(name: "include")
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
field1: ID!
|
||||
}
|
||||
`;
|
||||
|
||||
const sdl2 = /* GraphQL */ `
|
||||
schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@tag"]) {
|
||||
query: Query
|
||||
mutation: Mutation
|
||||
}
|
||||
|
||||
type Query {
|
||||
field2: String! @tag(name: "include")
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
field2: ID!
|
||||
}
|
||||
`;
|
||||
|
||||
const result = await compose({
|
||||
contracts: [
|
||||
{
|
||||
filter: {
|
||||
include: ['include'],
|
||||
exclude: null,
|
||||
removeUnreachableTypesFromPublicApiSchema: true,
|
||||
},
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
external: null,
|
||||
native: true,
|
||||
requestId: '1',
|
||||
schemas: [
|
||||
{
|
||||
raw: sdl,
|
||||
source: 'foo.graphql',
|
||||
url: 'https://lol.de',
|
||||
},
|
||||
{
|
||||
raw: sdl2,
|
||||
source: 'foo2.graphql',
|
||||
url: 'https://trolol.de',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.type).toEqual('success');
|
||||
const contractResult = result.result.contracts?.at(0);
|
||||
expect(contractResult?.id).toEqual('1');
|
||||
expect(contractResult?.result.type).toEqual('success');
|
||||
expect(contractResult?.result.result.sdl).toMatchInlineSnapshot(`
|
||||
type Query {
|
||||
field1: String!
|
||||
field2: String!
|
||||
}
|
||||
`);
|
||||
expect(contractResult?.result.result.supergraph).toMatchInlineSnapshot(`
|
||||
schema @link(url: "https://specs.apollo.dev/link/v1.0") @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY) {
|
||||
query: Query
|
||||
mutation: Mutation
|
||||
}
|
||||
|
||||
directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
|
||||
|
||||
directive @join__graph(name: String!, url: String!) on ENUM_VALUE
|
||||
|
||||
directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
|
||||
|
||||
directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE
|
||||
|
||||
directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
|
||||
|
||||
directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
|
||||
|
||||
scalar join__FieldSet
|
||||
|
||||
directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
|
||||
|
||||
scalar link__Import
|
||||
|
||||
enum link__Purpose {
|
||||
"""
|
||||
\`SECURITY\` features provide metadata necessary to securely resolve fields.
|
||||
"""
|
||||
SECURITY
|
||||
"""
|
||||
\`EXECUTION\` features provide metadata necessary for operation execution.
|
||||
"""
|
||||
EXECUTION
|
||||
}
|
||||
|
||||
directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ENUM | ENUM_VALUE | SCALAR | INPUT_OBJECT | INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION
|
||||
|
||||
enum join__Graph {
|
||||
FOO_GRAPHQL @join__graph(name: "foo.graphql", url: "https://lol.de")
|
||||
FOO2_GRAPHQL @join__graph(name: "foo2.graphql", url: "https://trolol.de")
|
||||
}
|
||||
|
||||
type Query @join__type(graph: FOO_GRAPHQL) @join__type(graph: FOO2_GRAPHQL) {
|
||||
field1: String! @join__field(graph: FOO_GRAPHQL)
|
||||
field2: String! @join__field(graph: FOO2_GRAPHQL)
|
||||
}
|
||||
|
||||
type Mutation @join__type(graph: FOO_GRAPHQL) @join__type(graph: FOO2_GRAPHQL) @inaccessible {
|
||||
field1: ID! @join__field(graph: FOO_GRAPHQL) @inaccessible
|
||||
field2: ID! @join__field(graph: FOO2_GRAPHQL) @inaccessible
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('contract: mutation type is part of the public schema if at least one field is included', async () => {
|
||||
const compose = createComposeFederation({
|
||||
decrypt: () => '',
|
||||
requestTimeoutMs: Infinity,
|
||||
});
|
||||
|
||||
const sdl = /* GraphQL */ `
|
||||
schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@tag"]) {
|
||||
query: Query
|
||||
mutation: Mutation
|
||||
}
|
||||
|
||||
type Query {
|
||||
field1: String! @tag(name: "include")
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
field1: ID!
|
||||
}
|
||||
`;
|
||||
|
||||
const sdl2 = /* GraphQL */ `
|
||||
schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@tag"]) {
|
||||
query: Query
|
||||
mutation: Mutation
|
||||
}
|
||||
|
||||
type Query {
|
||||
field2: String! @tag(name: "include")
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
field2: ID! @tag(name: "include")
|
||||
}
|
||||
`;
|
||||
|
||||
const result = await compose({
|
||||
contracts: [
|
||||
{
|
||||
filter: {
|
||||
include: ['include'],
|
||||
exclude: null,
|
||||
removeUnreachableTypesFromPublicApiSchema: true,
|
||||
},
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
external: null,
|
||||
native: true,
|
||||
requestId: '1',
|
||||
schemas: [
|
||||
{
|
||||
raw: sdl,
|
||||
source: 'foo.graphql',
|
||||
url: 'https://lol.de',
|
||||
},
|
||||
{
|
||||
raw: sdl2,
|
||||
source: 'foo2.graphql',
|
||||
url: 'https://trolol.de',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.type).toEqual('success');
|
||||
const contractResult = result.result.contracts?.at(0);
|
||||
expect(contractResult?.id).toEqual('1');
|
||||
expect(contractResult?.result.type).toEqual('success');
|
||||
expect(contractResult?.result.result.sdl).toMatchInlineSnapshot(`
|
||||
type Query {
|
||||
field1: String!
|
||||
field2: String!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
field2: ID!
|
||||
}
|
||||
`);
|
||||
expect(contractResult?.result.result.supergraph).toMatchInlineSnapshot(`
|
||||
schema @link(url: "https://specs.apollo.dev/link/v1.0") @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY) {
|
||||
query: Query
|
||||
mutation: Mutation
|
||||
}
|
||||
|
||||
directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
|
||||
|
||||
directive @join__graph(name: String!, url: String!) on ENUM_VALUE
|
||||
|
||||
directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
|
||||
|
||||
directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE
|
||||
|
||||
directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
|
||||
|
||||
directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
|
||||
|
||||
scalar join__FieldSet
|
||||
|
||||
directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
|
||||
|
||||
scalar link__Import
|
||||
|
||||
enum link__Purpose {
|
||||
"""
|
||||
\`SECURITY\` features provide metadata necessary to securely resolve fields.
|
||||
"""
|
||||
SECURITY
|
||||
"""
|
||||
\`EXECUTION\` features provide metadata necessary for operation execution.
|
||||
"""
|
||||
EXECUTION
|
||||
}
|
||||
|
||||
directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ENUM | ENUM_VALUE | SCALAR | INPUT_OBJECT | INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION
|
||||
|
||||
enum join__Graph {
|
||||
FOO_GRAPHQL @join__graph(name: "foo.graphql", url: "https://lol.de")
|
||||
FOO2_GRAPHQL @join__graph(name: "foo2.graphql", url: "https://trolol.de")
|
||||
}
|
||||
|
||||
type Query @join__type(graph: FOO_GRAPHQL) @join__type(graph: FOO2_GRAPHQL) {
|
||||
field1: String! @join__field(graph: FOO_GRAPHQL)
|
||||
field2: String! @join__field(graph: FOO2_GRAPHQL)
|
||||
}
|
||||
|
||||
type Mutation @join__type(graph: FOO_GRAPHQL) @join__type(graph: FOO2_GRAPHQL) {
|
||||
field1: ID! @join__field(graph: FOO_GRAPHQL) @inaccessible
|
||||
field2: ID! @join__field(graph: FOO2_GRAPHQL)
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('contract: scalar is inaccessible, despite being included in at least one subraph', async () => {
|
||||
const compose = createComposeFederation({
|
||||
decrypt: () => '',
|
||||
requestTimeoutMs: Infinity,
|
||||
});
|
||||
|
||||
const sdl2 = /* GraphQL */ `
|
||||
schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@tag"]) {
|
||||
query: Query
|
||||
}
|
||||
|
||||
type Query {
|
||||
field1: Foo!
|
||||
}
|
||||
|
||||
type Foo {
|
||||
field: Brr @tag(name: "include")
|
||||
}
|
||||
|
||||
scalar Brr @tag(name: "include")
|
||||
`;
|
||||
|
||||
const sdl = /* GraphQL */ `
|
||||
schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@tag"]) {
|
||||
query: Query
|
||||
}
|
||||
|
||||
type Query {
|
||||
c: Int @tag(name: "include")
|
||||
}
|
||||
|
||||
scalar Brr
|
||||
`;
|
||||
|
||||
const result = await compose({
|
||||
contracts: [
|
||||
{
|
||||
filter: {
|
||||
include: ['include'],
|
||||
exclude: null,
|
||||
removeUnreachableTypesFromPublicApiSchema: false,
|
||||
},
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
external: null,
|
||||
native: true,
|
||||
requestId: '1',
|
||||
schemas: [
|
||||
{
|
||||
raw: sdl,
|
||||
source: 'foo.graphql',
|
||||
url: 'https://lol.de',
|
||||
},
|
||||
{
|
||||
raw: sdl2,
|
||||
source: 'foo2.graphql',
|
||||
url: 'https://trolol.de',
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(result.type).toEqual('success');
|
||||
const contractResult = result.result.contracts?.at(0);
|
||||
|
||||
expect(contractResult?.id).toEqual('1');
|
||||
expect(contractResult?.result.type).toEqual('success');
|
||||
|
||||
const line = contractResult?.result.result.supergraph
|
||||
?.split('\n')
|
||||
.find(line => line.includes('scalar Brr'));
|
||||
expect(line).toEqual(
|
||||
`scalar Brr @join__type(graph: FOO_GRAPHQL) @join__type(graph: FOO2_GRAPHQL) `,
|
||||
);
|
||||
});
|
||||
|
|
@ -7,9 +7,12 @@ import {
|
|||
type NameNode,
|
||||
} from 'graphql';
|
||||
import type { ServiceLogger } from '@hive/service-common';
|
||||
import { extractLinkImplementations } from '@theguild/federation-composition';
|
||||
import {
|
||||
addInaccessibleToUnreachableTypes,
|
||||
applyTagFilterOnSubgraphs,
|
||||
extractLinkImplementations,
|
||||
} from '@theguild/federation-composition';
|
||||
import type { ContractsInputType } from '../api';
|
||||
import { addInaccessibleToUnreachableTypes } from '../lib/add-inaccessible-to-unreachable-types';
|
||||
import {
|
||||
composeExternalFederation,
|
||||
composeFederationV1,
|
||||
|
|
@ -18,7 +21,6 @@ import {
|
|||
SubgraphInput,
|
||||
} from '../lib/compose';
|
||||
import {
|
||||
applyTagFilterOnSubgraphs,
|
||||
createTagDirectiveNameExtractionStrategy,
|
||||
extractTagsFromDocument,
|
||||
} from '../lib/federation-tag-extraction';
|
||||
|
|
@ -260,38 +262,21 @@ export const createComposeFederation = (deps: ComposeFederationDeps) =>
|
|||
) {
|
||||
let supergraphSDL = parse(compositionResult.result.supergraph);
|
||||
const { resolveImportName } = extractLinkImplementations(supergraphSDL);
|
||||
const result = addInaccessibleToUnreachableTypes(
|
||||
resolveImportName,
|
||||
compositionResult,
|
||||
supergraphSDL,
|
||||
);
|
||||
|
||||
if (result.type === 'success') {
|
||||
return {
|
||||
id: contract.id,
|
||||
result: {
|
||||
type: 'success',
|
||||
result: {
|
||||
supergraph: result.result.supergraph,
|
||||
sdl: result.result.sdl,
|
||||
},
|
||||
},
|
||||
} satisfies ContractResultSuccess;
|
||||
}
|
||||
const result = addInaccessibleToUnreachableTypes(resolveImportName, {
|
||||
supergraphSdl: compositionResult.result.supergraph,
|
||||
publicSdl: compositionResult.result.sdl,
|
||||
});
|
||||
|
||||
return {
|
||||
id: contract.id,
|
||||
result: {
|
||||
type: 'failure',
|
||||
type: 'success',
|
||||
result: {
|
||||
supergraph: null,
|
||||
sdl: null,
|
||||
errors: result.result.errors,
|
||||
includesNetworkError: false,
|
||||
includesException: false,
|
||||
supergraph: result.supergraphSdl,
|
||||
sdl: result.publicSdl,
|
||||
},
|
||||
},
|
||||
} satisfies ContractResultFailure;
|
||||
} satisfies ContractResultSuccess;
|
||||
}
|
||||
|
||||
if (compositionResult.type === 'success') {
|
||||
|
|
|
|||
|
|
@ -1,101 +0,0 @@
|
|||
import { parse } from 'graphql';
|
||||
import { extractLinkImplementations } from '@theguild/federation-composition';
|
||||
import { addInaccessibleToUnreachableTypes } from '../add-inaccessible-to-unreachable-types';
|
||||
import { composeFederationV2 } from '../compose';
|
||||
|
||||
test('Filters based on tags', async () => {
|
||||
let compositionResult = composeFederationV2(
|
||||
[
|
||||
{
|
||||
name: 'user',
|
||||
url: 'https://user-example.graphql-hive.com',
|
||||
typeDefs: parse(`
|
||||
type Query {
|
||||
user: User
|
||||
}
|
||||
type User {
|
||||
id: ID!
|
||||
name: String
|
||||
}
|
||||
|
||||
type Unused {
|
||||
foo: String
|
||||
}
|
||||
`),
|
||||
},
|
||||
],
|
||||
console as any,
|
||||
);
|
||||
|
||||
expect(compositionResult.type).toBe('success');
|
||||
if (compositionResult.type === 'success') {
|
||||
let supergraphSDL = parse(compositionResult.result.supergraph);
|
||||
const { resolveImportName } = extractLinkImplementations(supergraphSDL);
|
||||
compositionResult = addInaccessibleToUnreachableTypes(
|
||||
resolveImportName,
|
||||
compositionResult,
|
||||
supergraphSDL,
|
||||
);
|
||||
expect(compositionResult.result.supergraph).toMatchInlineSnapshot(`
|
||||
schema @link(url: "https://specs.apollo.dev/link/v1.0") @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) {
|
||||
query: Query
|
||||
}
|
||||
|
||||
directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
|
||||
|
||||
directive @join__graph(name: String!, url: String!) on ENUM_VALUE
|
||||
|
||||
directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
|
||||
|
||||
directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE
|
||||
|
||||
directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
|
||||
|
||||
directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
|
||||
|
||||
scalar join__FieldSet
|
||||
|
||||
directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
|
||||
|
||||
scalar link__Import
|
||||
|
||||
enum link__Purpose {
|
||||
"""
|
||||
\`SECURITY\` features provide metadata necessary to securely resolve fields.
|
||||
"""
|
||||
SECURITY
|
||||
"""
|
||||
\`EXECUTION\` features provide metadata necessary for operation execution.
|
||||
"""
|
||||
EXECUTION
|
||||
}
|
||||
|
||||
enum join__Graph {
|
||||
USER @join__graph(name: "user", url: "https://user-example.graphql-hive.com")
|
||||
}
|
||||
|
||||
type Query @join__type(graph: USER) {
|
||||
user: User
|
||||
}
|
||||
|
||||
type User @join__type(graph: USER) {
|
||||
id: ID!
|
||||
name: String
|
||||
}
|
||||
|
||||
type Unused @join__type(graph: USER) @inaccessible {
|
||||
foo: String
|
||||
}
|
||||
`);
|
||||
expect(compositionResult.result.sdl).toMatchInlineSnapshot(`
|
||||
type Query {
|
||||
user: User
|
||||
}
|
||||
|
||||
type User {
|
||||
id: ID!
|
||||
name: String
|
||||
}
|
||||
`);
|
||||
}
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,320 +0,0 @@
|
|||
import { parse, print } from 'graphql';
|
||||
import { addDirectiveOnTypes, getReachableTypes } from '../reachable-type-filter';
|
||||
|
||||
describe('getReachableTypes', () => {
|
||||
it('includes the query type', () => {
|
||||
const documentNode = parse(/* GraphQL */ `
|
||||
type Query {
|
||||
hello: String
|
||||
}
|
||||
`);
|
||||
const reachableTypes = getReachableTypes(documentNode);
|
||||
expect(reachableTypes.size).toEqual(1);
|
||||
expect(reachableTypes.has('Query')).toEqual(true);
|
||||
});
|
||||
it('includes the mutation type', () => {
|
||||
const documentNode = parse(/* GraphQL */ `
|
||||
type Mutation {
|
||||
hello: String
|
||||
}
|
||||
`);
|
||||
const reachableTypes = getReachableTypes(documentNode);
|
||||
expect(reachableTypes.size).toEqual(1);
|
||||
expect(reachableTypes.has('Mutation')).toEqual(true);
|
||||
});
|
||||
it('includes the subscription type', () => {
|
||||
const documentNode = parse(/* GraphQL */ `
|
||||
type Subscription {
|
||||
hello: String
|
||||
}
|
||||
`);
|
||||
const reachableTypes = getReachableTypes(documentNode);
|
||||
expect(reachableTypes.size).toEqual(1);
|
||||
expect(reachableTypes.has('Subscription')).toEqual(true);
|
||||
});
|
||||
it('excludes unused root type', () => {
|
||||
const documentNode = parse(/* GraphQL */ `
|
||||
type Query {
|
||||
hello: String
|
||||
}
|
||||
type Mutation {
|
||||
hello: String
|
||||
}
|
||||
|
||||
schema {
|
||||
query: Query
|
||||
}
|
||||
`);
|
||||
const reachableTypes = getReachableTypes(documentNode);
|
||||
expect(reachableTypes.size).toEqual(1);
|
||||
expect(reachableTypes.has('Query')).toEqual(true);
|
||||
expect(reachableTypes.has('Mutation')).toEqual(false);
|
||||
});
|
||||
it('includes object types referenced by root type', () => {
|
||||
const documentNode = parse(/* GraphQL */ `
|
||||
type Query {
|
||||
hello: Hello
|
||||
}
|
||||
type Hello {
|
||||
world: String
|
||||
}
|
||||
`);
|
||||
const reachableTypes = getReachableTypes(documentNode);
|
||||
expect(reachableTypes.size).toEqual(2);
|
||||
expect(reachableTypes.has('Query')).toEqual(true);
|
||||
expect(reachableTypes.has('Hello')).toEqual(true);
|
||||
});
|
||||
it('includes scalar types referenced by root type', () => {
|
||||
const documentNode = parse(/* GraphQL */ `
|
||||
type Query {
|
||||
hello: Hello
|
||||
}
|
||||
scalar Hello
|
||||
`);
|
||||
const reachableTypes = getReachableTypes(documentNode);
|
||||
expect(reachableTypes.size).toEqual(2);
|
||||
expect(reachableTypes.has('Query')).toEqual(true);
|
||||
expect(reachableTypes.has('Hello')).toEqual(true);
|
||||
});
|
||||
it('includes input types referenced by root type', () => {
|
||||
const documentNode = parse(/* GraphQL */ `
|
||||
type Query {
|
||||
hello(input: Hello): String
|
||||
}
|
||||
input Hello {
|
||||
world: String
|
||||
}
|
||||
`);
|
||||
const reachableTypes = getReachableTypes(documentNode);
|
||||
expect(reachableTypes.size).toEqual(2);
|
||||
expect(reachableTypes.has('Query')).toEqual(true);
|
||||
expect(reachableTypes.has('Hello')).toEqual(true);
|
||||
});
|
||||
it('includes enum types referenced by root type', () => {
|
||||
const documentNode = parse(/* GraphQL */ `
|
||||
type Query {
|
||||
hello: Hello
|
||||
}
|
||||
enum Hello {
|
||||
WORLD
|
||||
}
|
||||
`);
|
||||
const reachableTypes = getReachableTypes(documentNode);
|
||||
expect(reachableTypes.size).toEqual(2);
|
||||
expect(reachableTypes.has('Query')).toEqual(true);
|
||||
expect(reachableTypes.has('Hello')).toEqual(true);
|
||||
});
|
||||
it('includes union type and union members referenced by root type', () => {
|
||||
const documentNode = parse(/* GraphQL */ `
|
||||
type Query {
|
||||
hello: Hello
|
||||
}
|
||||
union Hello = World
|
||||
union Gang = World
|
||||
type World {
|
||||
world: String
|
||||
}
|
||||
`);
|
||||
const reachableTypes = getReachableTypes(documentNode);
|
||||
expect(reachableTypes.size).toEqual(3);
|
||||
expect(reachableTypes.has('Query')).toEqual(true);
|
||||
expect(reachableTypes.has('Hello')).toEqual(true);
|
||||
expect(reachableTypes.has('World')).toEqual(true);
|
||||
});
|
||||
it('includes interface type and interface members referenced by root type', () => {
|
||||
const documentNode = parse(/* GraphQL */ `
|
||||
type Query {
|
||||
hello: Hello
|
||||
}
|
||||
interface Hello {
|
||||
world: String
|
||||
}
|
||||
type World implements Hello {
|
||||
world: String
|
||||
}
|
||||
`);
|
||||
const reachableTypes = getReachableTypes(documentNode);
|
||||
expect(reachableTypes.size).toEqual(3);
|
||||
expect(reachableTypes.has('Query')).toEqual(true);
|
||||
expect(reachableTypes.has('Hello')).toEqual(true);
|
||||
expect(reachableTypes.has('World')).toEqual(true);
|
||||
});
|
||||
it('includes input type referenced by input type', () => {
|
||||
const documentNode = parse(/* GraphQL */ `
|
||||
input Hello {
|
||||
world: World
|
||||
}
|
||||
input World {
|
||||
world: String
|
||||
}
|
||||
type Query {
|
||||
hello(world: Hello): String
|
||||
}
|
||||
`);
|
||||
const reachableTypes = getReachableTypes(documentNode);
|
||||
expect(reachableTypes.size).toEqual(3);
|
||||
expect(reachableTypes.has('Query')).toEqual(true);
|
||||
expect(reachableTypes.has('Hello')).toEqual(true);
|
||||
expect(reachableTypes.has('World')).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addDirectiveOnTypes', () => {
|
||||
it('add directive on unused root type', () => {
|
||||
const documentNode = parse(/* GraphQL */ `
|
||||
type Query {
|
||||
hello: String
|
||||
}
|
||||
type Mutation {
|
||||
hello: String
|
||||
}
|
||||
|
||||
schema {
|
||||
query: Query
|
||||
}
|
||||
`);
|
||||
const document = addDirectiveOnTypes({
|
||||
documentNode,
|
||||
excludedTypeNames: getReachableTypes(documentNode),
|
||||
directiveName: 'inaccessible',
|
||||
});
|
||||
expect(print(document)).toMatchInlineSnapshot(`
|
||||
type Query {
|
||||
hello: String
|
||||
}
|
||||
|
||||
type Mutation @inaccessible {
|
||||
hello: String
|
||||
}
|
||||
|
||||
schema {
|
||||
query: Query
|
||||
}
|
||||
`);
|
||||
});
|
||||
it('does not re-apply directive', () => {
|
||||
const documentNode = parse(/* GraphQL */ `
|
||||
type Query {
|
||||
hello: String
|
||||
}
|
||||
|
||||
type Mutation @inaccessible {
|
||||
hello: String
|
||||
}
|
||||
|
||||
schema {
|
||||
query: Query
|
||||
}
|
||||
|
||||
directive @inaccessible on OBJECT
|
||||
`);
|
||||
const document = addDirectiveOnTypes({
|
||||
documentNode,
|
||||
excludedTypeNames: getReachableTypes(documentNode),
|
||||
directiveName: 'inaccessible',
|
||||
});
|
||||
expect(print(document)).toMatchInlineSnapshot(`
|
||||
type Query {
|
||||
hello: String
|
||||
}
|
||||
|
||||
type Mutation @inaccessible {
|
||||
hello: String
|
||||
}
|
||||
|
||||
schema {
|
||||
query: Query
|
||||
}
|
||||
|
||||
directive @inaccessible on OBJECT
|
||||
`);
|
||||
});
|
||||
it('runs on supergraph', () => {
|
||||
// This is technically not a fully valid supergraph document node.
|
||||
// It only includes the minimum required types and directives to test the functionality.
|
||||
const documentNode = parse(/* GraphQL */ `
|
||||
type Query @join__type(graph: BAR_GRAPHQL) {
|
||||
bar: Car @inaccessible
|
||||
barHidden: String
|
||||
}
|
||||
|
||||
type Bar @join__type(graph: BAR_GRAPHQL) {
|
||||
hello: String
|
||||
helloHidden: String
|
||||
}
|
||||
|
||||
type Car @join__type(graph: BAR_GRAPHQL) {
|
||||
hello: String @inaccessible
|
||||
helloHidden: String
|
||||
}
|
||||
|
||||
schema {
|
||||
query: Query
|
||||
}
|
||||
|
||||
####
|
||||
# Note: all the directives and types below are part of a supergraph schema
|
||||
####
|
||||
|
||||
directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ENUM | ENUM_VALUE | SCALAR | INPUT_OBJECT | INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION
|
||||
scalar join__FieldSet
|
||||
directive @join__type(
|
||||
graph: join__Graph!
|
||||
key: join__FieldSet
|
||||
extension: Boolean! = false
|
||||
resolvable: Boolean! = true
|
||||
isInterfaceObject: Boolean! = false
|
||||
) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
|
||||
|
||||
directive @join__graph(name: String!, url: String!) on ENUM_VALUE
|
||||
|
||||
enum join__Graph {
|
||||
BAR_GRAPHQL @join__graph(name: "bar.graphql", url: "")
|
||||
}
|
||||
`);
|
||||
|
||||
const excludedTypeNames = getReachableTypes(documentNode);
|
||||
|
||||
excludedTypeNames.add('join__Graph');
|
||||
excludedTypeNames.add('join__FieldSet');
|
||||
|
||||
const document = addDirectiveOnTypes({
|
||||
documentNode,
|
||||
excludedTypeNames,
|
||||
directiveName: 'inaccessible',
|
||||
});
|
||||
|
||||
expect(print(document)).toMatchInlineSnapshot(`
|
||||
type Query @join__type(graph: BAR_GRAPHQL) {
|
||||
bar: Car @inaccessible
|
||||
barHidden: String
|
||||
}
|
||||
|
||||
type Bar @join__type(graph: BAR_GRAPHQL) @inaccessible {
|
||||
hello: String
|
||||
helloHidden: String
|
||||
}
|
||||
|
||||
type Car @join__type(graph: BAR_GRAPHQL) {
|
||||
hello: String @inaccessible
|
||||
helloHidden: String
|
||||
}
|
||||
|
||||
schema {
|
||||
query: Query
|
||||
}
|
||||
|
||||
directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ENUM | ENUM_VALUE | SCALAR | INPUT_OBJECT | INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION
|
||||
|
||||
scalar join__FieldSet
|
||||
|
||||
directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
|
||||
|
||||
directive @join__graph(name: String!, url: String!) on ENUM_VALUE
|
||||
|
||||
enum join__Graph {
|
||||
BAR_GRAPHQL @join__graph(name: "bar.graphql", url: "")
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
import { DocumentNode, parse, print } from 'graphql';
|
||||
import { transformSupergraphToPublicSchema } from '@theguild/federation-composition';
|
||||
import { ComposerMethodResult } from './compose';
|
||||
import { addDirectiveOnTypes, getReachableTypes } from './reachable-type-filter';
|
||||
|
||||
/**
|
||||
* Adds inaccessible directive to unreachable types
|
||||
*
|
||||
* @param resolveName
|
||||
* @param compositionResult
|
||||
* @param supergraphSDL
|
||||
* @returns
|
||||
*/
|
||||
|
||||
export const addInaccessibleToUnreachableTypes = (
|
||||
resolveName: (identity: string, name: string) => string,
|
||||
compositionResult: ComposerMethodResult,
|
||||
supergraphSDL: DocumentNode,
|
||||
): ComposerMethodResult => {
|
||||
const inaccessibleDirectiveName = resolveName(
|
||||
'https://specs.apollo.dev/inaccessible',
|
||||
'@inaccessible',
|
||||
);
|
||||
const federationTypes = new Set([
|
||||
resolveName('https://specs.apollo.dev/join', 'FieldSet'),
|
||||
resolveName('https://specs.apollo.dev/join', 'Graph'),
|
||||
resolveName('https://specs.apollo.dev/link', 'Import'),
|
||||
resolveName('https://specs.apollo.dev/link', 'Purpose'),
|
||||
resolveName('https://specs.apollo.dev/federation', 'Policy'),
|
||||
resolveName('https://specs.apollo.dev/federation', 'Scope'),
|
||||
resolveName('https://specs.apollo.dev/join', 'DirectiveArguments'),
|
||||
]);
|
||||
|
||||
if (compositionResult.type === 'failure' || inaccessibleDirectiveName === undefined) {
|
||||
// In case there is no inaccessible directive, we can't remove types from the public api schema as everything is reachable.
|
||||
return compositionResult;
|
||||
}
|
||||
|
||||
// we retrieve the list of reachable types from the public api sdl
|
||||
const reachableTypeNames = getReachableTypes(parse(compositionResult.result.sdl!));
|
||||
// apollo router does not like @inaccessible on federation types...
|
||||
for (const federationType of federationTypes) {
|
||||
reachableTypeNames.add(federationType);
|
||||
}
|
||||
|
||||
// then we apply the filter to the supergraph SDL (which is the source for the public api sdl)
|
||||
supergraphSDL = addDirectiveOnTypes({
|
||||
documentNode: supergraphSDL,
|
||||
excludedTypeNames: reachableTypeNames,
|
||||
directiveName: inaccessibleDirectiveName,
|
||||
});
|
||||
return {
|
||||
...compositionResult,
|
||||
result: {
|
||||
...compositionResult.result,
|
||||
supergraph: print(supergraphSDL),
|
||||
sdl: print(transformSupergraphToPublicSchema(supergraphSDL)),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
@ -1,418 +1,18 @@
|
|||
import {
|
||||
EnumTypeExtensionNode,
|
||||
EnumValueDefinitionNode,
|
||||
InputObjectTypeExtensionNode,
|
||||
InterfaceTypeExtensionNode,
|
||||
Kind,
|
||||
NameNode,
|
||||
ObjectTypeExtensionNode,
|
||||
visit,
|
||||
type ConstDirectiveNode,
|
||||
type DirectiveNode,
|
||||
type DocumentNode,
|
||||
type EnumTypeDefinitionNode,
|
||||
type EnumValueDefinitionNode,
|
||||
type FieldDefinitionNode,
|
||||
type InputObjectTypeDefinitionNode,
|
||||
type InputValueDefinitionNode,
|
||||
type InterfaceTypeDefinitionNode,
|
||||
type ObjectTypeDefinitionNode,
|
||||
type ScalarTypeDefinitionNode,
|
||||
type UnionTypeDefinitionNode,
|
||||
type NameNode,
|
||||
} from 'graphql';
|
||||
import { extractLinkImplementations } from '@theguild/federation-composition';
|
||||
|
||||
type TagExtractionStrategy = (directiveNode: DirectiveNode) => string | null;
|
||||
|
||||
function createTransformTagDirectives(tagDirectiveName: string, inaccessibleDirectiveName: string) {
|
||||
return function transformTagDirectives(
|
||||
node: { directives?: readonly ConstDirectiveNode[] },
|
||||
/** if non-null, will add the inaccessible directive to the nodes directive if not already present. */
|
||||
includeInaccessibleDirective: boolean = false,
|
||||
): readonly ConstDirectiveNode[] {
|
||||
let hasInaccessibleDirective = false;
|
||||
const directives =
|
||||
node.directives?.filter(directive => {
|
||||
if (directive.name.value === inaccessibleDirectiveName) {
|
||||
hasInaccessibleDirective = true;
|
||||
}
|
||||
return directive.name.value !== tagDirectiveName;
|
||||
}) ?? [];
|
||||
|
||||
if (hasInaccessibleDirective === false && includeInaccessibleDirective) {
|
||||
directives.push({
|
||||
kind: Kind.DIRECTIVE,
|
||||
name: {
|
||||
kind: Kind.NAME,
|
||||
value: inaccessibleDirectiveName,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return directives;
|
||||
};
|
||||
}
|
||||
|
||||
/** check whether two sets have an intersection with each other. */
|
||||
function hasIntersection<T>(a: Set<T>, b: Set<T>): boolean {
|
||||
if (a.size === 0 || b.size === 0) {
|
||||
return false;
|
||||
}
|
||||
for (const item of a) {
|
||||
if (b.has(item)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getRootQueryTypeNameFromDocumentNode(document: DocumentNode) {
|
||||
let queryName: string | null = 'Query';
|
||||
|
||||
for (const definition of document.definitions) {
|
||||
if (definition.kind === Kind.SCHEMA_DEFINITION || definition.kind === Kind.SCHEMA_EXTENSION) {
|
||||
for (const operationTypeDefinition of definition.operationTypes ?? []) {
|
||||
if (operationTypeDefinition.operation === 'query') {
|
||||
queryName = operationTypeDefinition.type.name.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return queryName;
|
||||
}
|
||||
|
||||
type ObjectLikeNode =
|
||||
| ObjectTypeExtensionNode
|
||||
| ObjectTypeDefinitionNode
|
||||
| InterfaceTypeDefinitionNode
|
||||
| InterfaceTypeExtensionNode
|
||||
| InputObjectTypeDefinitionNode
|
||||
| InputObjectTypeExtensionNode;
|
||||
|
||||
/**
|
||||
* Takes a subgraph document node and a set of tag filters and transforms the document node to contain `@inaccessible` directives on all fields not included by the applied filter.
|
||||
* Note: you probably want to use `filterSubgraphs` instead, as it also applies the correct post step required after applying this.
|
||||
*/
|
||||
export function applyTagFilterToInaccessibleTransformOnSubgraphSchema(
|
||||
documentNode: DocumentNode,
|
||||
tagRegister: SchemaCoordinateToTagsRegistry,
|
||||
filter: Federation2SubgraphDocumentNodeByTagsFilter,
|
||||
): {
|
||||
typeDefs: DocumentNode;
|
||||
/** Types within THIS subgraph where all fields are inaccessible */
|
||||
typesWithAllFieldsInaccessible: Map<string, boolean>;
|
||||
transformTagDirectives: ReturnType<typeof createTransformTagDirectives>;
|
||||
/** Types in this subgraph that have the inaccessible directive applied */
|
||||
typesWithInaccessibleApplied: Set<string>;
|
||||
} {
|
||||
const { resolveImportName } = extractLinkImplementations(documentNode);
|
||||
const inaccessibleDirectiveName = resolveImportName(
|
||||
'https://specs.apollo.dev/federation',
|
||||
'@inaccessible',
|
||||
);
|
||||
const tagDirectiveName = resolveImportName('https://specs.apollo.dev/federation', '@tag');
|
||||
const externalDirectiveName = resolveImportName(
|
||||
'https://specs.apollo.dev/federation',
|
||||
'@external',
|
||||
);
|
||||
|
||||
function getTagsForSchemaCoordinate(coordinate: string) {
|
||||
return tagRegister.get(coordinate) ?? new Set();
|
||||
}
|
||||
|
||||
const transformTagDirectives = createTransformTagDirectives(
|
||||
tagDirectiveName,
|
||||
inaccessibleDirectiveName,
|
||||
);
|
||||
const rootQueryTypeName = getRootQueryTypeNameFromDocumentNode(documentNode);
|
||||
|
||||
const typesWithAllFieldsInaccessibleTracker = new Map<string, boolean>();
|
||||
|
||||
function onAllFieldsInaccessible(name: string) {
|
||||
const current = typesWithAllFieldsInaccessibleTracker.get(name);
|
||||
if (current === undefined) {
|
||||
typesWithAllFieldsInaccessibleTracker.set(name, true);
|
||||
}
|
||||
}
|
||||
|
||||
function onSomeFieldsAccessible(name: string) {
|
||||
typesWithAllFieldsInaccessibleTracker.set(name, false);
|
||||
}
|
||||
|
||||
function fieldArgumentHandler(
|
||||
objectLikeNode: ObjectLikeNode,
|
||||
fieldLikeNode: InputValueDefinitionNode | FieldDefinitionNode,
|
||||
node: InputValueDefinitionNode,
|
||||
) {
|
||||
// Check for external tag because we cannot contribute directives to external fields.
|
||||
if (node.directives?.find(d => d.name.value === externalDirectiveName)) {
|
||||
return node;
|
||||
}
|
||||
|
||||
const tagsOnNode = getTagsForSchemaCoordinate(
|
||||
`${objectLikeNode.name.value}.${fieldLikeNode.name.value}(${node.name.value}:)`,
|
||||
);
|
||||
|
||||
if (
|
||||
(filter.include.size && !hasIntersection(tagsOnNode, filter.include)) ||
|
||||
(filter.exclude.size && hasIntersection(tagsOnNode, filter.exclude))
|
||||
) {
|
||||
return {
|
||||
...node,
|
||||
directives: transformTagDirectives(node, true),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...node,
|
||||
directives: transformTagDirectives(node),
|
||||
};
|
||||
}
|
||||
|
||||
const definitionsBySchemaCoordinate = new Map<string, Array<ObjectLikeNode>>();
|
||||
|
||||
//
|
||||
// A type can be defined multiple times within a subgraph and we need to find all implementations
|
||||
// for determining whether the full type, or only some fields are part of the public contract schema
|
||||
//
|
||||
for (const definition of documentNode.definitions) {
|
||||
switch (definition.kind) {
|
||||
case Kind.OBJECT_TYPE_DEFINITION:
|
||||
case Kind.OBJECT_TYPE_EXTENSION:
|
||||
case Kind.INTERFACE_TYPE_DEFINITION:
|
||||
case Kind.INTERFACE_TYPE_EXTENSION:
|
||||
case Kind.INPUT_OBJECT_TYPE_DEFINITION:
|
||||
case Kind.INPUT_OBJECT_TYPE_EXTENSION: {
|
||||
let items = definitionsBySchemaCoordinate.get(definition.name.value);
|
||||
if (!items) {
|
||||
items = [];
|
||||
definitionsBySchemaCoordinate.set(definition.name.value, items);
|
||||
}
|
||||
items.push(definition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tracking for which type already has `@inaccessible` applied (can only occur once)
|
||||
const typesWithInaccessibleApplied = new Set<string>();
|
||||
// These are later used within the visitor to actually replace the nodes.
|
||||
const replacementTypeNodes = new Map<ObjectLikeNode, ObjectLikeNode>();
|
||||
|
||||
for (const [typeName, nodes] of definitionsBySchemaCoordinate) {
|
||||
/** After processing all nodes implementing a type, we know whether all or only some fields are inaccessible */
|
||||
let isSomeFieldsAccessible = false;
|
||||
|
||||
for (const node of nodes) {
|
||||
const tagsOnNode = getTagsForSchemaCoordinate(node.name.value);
|
||||
|
||||
let newNode = {
|
||||
...node,
|
||||
fields: node.fields?.map(fieldNode => {
|
||||
const tagsOnNode = getTagsForSchemaCoordinate(
|
||||
`${node.name.value}.${fieldNode.name.value}`,
|
||||
);
|
||||
|
||||
if (fieldNode.kind === Kind.FIELD_DEFINITION) {
|
||||
fieldNode = {
|
||||
...fieldNode,
|
||||
arguments: fieldNode.arguments?.map(argumentNode =>
|
||||
fieldArgumentHandler(node, fieldNode, argumentNode),
|
||||
),
|
||||
} as FieldDefinitionNode;
|
||||
}
|
||||
|
||||
// Check for external tag because we cannot contribute directives to external fields.
|
||||
if (fieldNode.directives?.find(d => d.name.value === externalDirectiveName)) {
|
||||
return fieldNode;
|
||||
}
|
||||
|
||||
if (
|
||||
(filter.include.size && !hasIntersection(tagsOnNode, filter.include)) ||
|
||||
(filter.exclude.size && hasIntersection(tagsOnNode, filter.exclude))
|
||||
) {
|
||||
return {
|
||||
...fieldNode,
|
||||
directives: transformTagDirectives(fieldNode, true),
|
||||
};
|
||||
}
|
||||
|
||||
isSomeFieldsAccessible = true;
|
||||
|
||||
return {
|
||||
...fieldNode,
|
||||
directives: transformTagDirectives(fieldNode),
|
||||
};
|
||||
}),
|
||||
} as ObjectLikeNode;
|
||||
|
||||
if (filter.exclude.size && hasIntersection(tagsOnNode, filter.exclude)) {
|
||||
newNode = {
|
||||
...newNode,
|
||||
directives: transformTagDirectives(
|
||||
node,
|
||||
typesWithInaccessibleApplied.has(typeName) ? false : true,
|
||||
),
|
||||
} as ObjectLikeNode;
|
||||
typesWithInaccessibleApplied.add(typeName);
|
||||
} else {
|
||||
newNode = {
|
||||
...newNode,
|
||||
directives: transformTagDirectives(node),
|
||||
};
|
||||
}
|
||||
|
||||
replacementTypeNodes.set(node, newNode);
|
||||
}
|
||||
|
||||
// If some fields are accessible, we continue with the next type
|
||||
if (isSomeFieldsAccessible) {
|
||||
onSomeFieldsAccessible(typeName);
|
||||
continue;
|
||||
}
|
||||
onAllFieldsInaccessible(typeName);
|
||||
}
|
||||
|
||||
function fieldLikeObjectHandler(node: ObjectLikeNode) {
|
||||
const newNode = replacementTypeNodes.get(node);
|
||||
if (!newNode) {
|
||||
throw new Error(
|
||||
`Found type without transformation mapping. ${node.name.value} ${node.name.kind}`,
|
||||
);
|
||||
}
|
||||
|
||||
return newNode;
|
||||
}
|
||||
|
||||
function enumHandler(node: EnumTypeDefinitionNode | EnumTypeExtensionNode) {
|
||||
const tagsOnNode = getTagsForSchemaCoordinate(node.name.value);
|
||||
|
||||
let isAllFieldsInaccessible = true;
|
||||
|
||||
const newNode = {
|
||||
...node,
|
||||
values: node.values?.map(valueNode => {
|
||||
const tagsOnNode = getTagsForSchemaCoordinate(`${node.name.value}.${valueNode.name.value}`);
|
||||
|
||||
if (
|
||||
(filter.include.size && !hasIntersection(tagsOnNode, filter.include)) ||
|
||||
(filter.exclude.size && hasIntersection(tagsOnNode, filter.exclude))
|
||||
) {
|
||||
return {
|
||||
...valueNode,
|
||||
directives: transformTagDirectives(valueNode, true),
|
||||
};
|
||||
}
|
||||
|
||||
isAllFieldsInaccessible = false;
|
||||
|
||||
return {
|
||||
...valueNode,
|
||||
directives: transformTagDirectives(valueNode),
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
if (filter.exclude.size && hasIntersection(tagsOnNode, filter.exclude)) {
|
||||
return {
|
||||
...newNode,
|
||||
directives: transformTagDirectives(node, true),
|
||||
};
|
||||
}
|
||||
|
||||
if (isAllFieldsInaccessible) {
|
||||
onAllFieldsInaccessible(node.name.value);
|
||||
} else {
|
||||
onSomeFieldsAccessible(node.name.value);
|
||||
}
|
||||
|
||||
return {
|
||||
...newNode,
|
||||
directives: transformTagDirectives(node),
|
||||
};
|
||||
}
|
||||
|
||||
function scalarAndUnionHandler(node: ScalarTypeDefinitionNode | UnionTypeDefinitionNode) {
|
||||
const tagsOnNode = getTagsForSchemaCoordinate(node.name.value);
|
||||
|
||||
if (
|
||||
(filter.include.size && !hasIntersection(tagsOnNode, filter.include)) ||
|
||||
(filter.exclude.size && hasIntersection(tagsOnNode, filter.exclude))
|
||||
) {
|
||||
return {
|
||||
...node,
|
||||
directives: transformTagDirectives(node, true),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...node,
|
||||
directives: transformTagDirectives(node),
|
||||
};
|
||||
}
|
||||
|
||||
const typeDefs = visit(documentNode, {
|
||||
[Kind.OBJECT_TYPE_DEFINITION]: fieldLikeObjectHandler,
|
||||
[Kind.OBJECT_TYPE_EXTENSION]: fieldLikeObjectHandler,
|
||||
[Kind.INTERFACE_TYPE_DEFINITION]: fieldLikeObjectHandler,
|
||||
[Kind.INTERFACE_TYPE_EXTENSION]: fieldLikeObjectHandler,
|
||||
[Kind.INPUT_OBJECT_TYPE_DEFINITION]: fieldLikeObjectHandler,
|
||||
[Kind.INPUT_OBJECT_TYPE_EXTENSION]: fieldLikeObjectHandler,
|
||||
[Kind.ENUM_TYPE_DEFINITION]: enumHandler,
|
||||
[Kind.ENUM_TYPE_EXTENSION]: enumHandler,
|
||||
[Kind.SCALAR_TYPE_DEFINITION]: scalarAndUnionHandler,
|
||||
[Kind.UNION_TYPE_DEFINITION]: scalarAndUnionHandler,
|
||||
});
|
||||
|
||||
typesWithAllFieldsInaccessibleTracker.delete(rootQueryTypeName);
|
||||
|
||||
return {
|
||||
typeDefs,
|
||||
typesWithAllFieldsInaccessible: typesWithAllFieldsInaccessibleTracker,
|
||||
transformTagDirectives,
|
||||
typesWithInaccessibleApplied,
|
||||
};
|
||||
}
|
||||
|
||||
function makeTypesFromSetInaccessible(
|
||||
documentNode: DocumentNode,
|
||||
types: Set<string>,
|
||||
transformTagDirectives: ReturnType<typeof createTransformTagDirectives>,
|
||||
) {
|
||||
/** We can only apply `@inaccessible` once on each unique typename, otherwise we get a composition error */
|
||||
const alreadyAppliedOnTypeNames = new Set<string>();
|
||||
function typeHandler(
|
||||
node:
|
||||
| ObjectTypeExtensionNode
|
||||
| ObjectTypeDefinitionNode
|
||||
| InterfaceTypeDefinitionNode
|
||||
| InterfaceTypeExtensionNode
|
||||
| InputObjectTypeDefinitionNode
|
||||
| InputObjectTypeExtensionNode
|
||||
| EnumTypeDefinitionNode
|
||||
| EnumTypeExtensionNode,
|
||||
) {
|
||||
if (types.has(node.name.value) === false || alreadyAppliedOnTypeNames.has(node.name.value)) {
|
||||
return;
|
||||
}
|
||||
alreadyAppliedOnTypeNames.add(node.name.value);
|
||||
return {
|
||||
...node,
|
||||
directives: transformTagDirectives(node, true),
|
||||
};
|
||||
}
|
||||
|
||||
return visit(documentNode, {
|
||||
[Kind.OBJECT_TYPE_DEFINITION]: typeHandler,
|
||||
[Kind.OBJECT_TYPE_EXTENSION]: typeHandler,
|
||||
[Kind.INTERFACE_TYPE_DEFINITION]: typeHandler,
|
||||
[Kind.INTERFACE_TYPE_EXTENSION]: typeHandler,
|
||||
[Kind.INPUT_OBJECT_TYPE_DEFINITION]: typeHandler,
|
||||
[Kind.INPUT_OBJECT_TYPE_EXTENSION]: typeHandler,
|
||||
[Kind.ENUM_TYPE_DEFINITION]: typeHandler,
|
||||
[Kind.ENUM_TYPE_EXTENSION]: typeHandler,
|
||||
});
|
||||
}
|
||||
|
||||
function collectTagsBySchemaCoordinateFromSubgraph(
|
||||
documentNode: DocumentNode,
|
||||
/** This map will be populated with values. */
|
||||
|
|
@ -573,81 +173,6 @@ export function buildSchemaCoordinateTagRegister(
|
|||
return schemaCoordinatesToTags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a tag filter to a set of subgraphs.
|
||||
*/
|
||||
export function applyTagFilterOnSubgraphs<
|
||||
TType extends {
|
||||
typeDefs: DocumentNode;
|
||||
name: string;
|
||||
},
|
||||
>(subgraphs: Array<TType>, filter: Federation2SubgraphDocumentNodeByTagsFilter): Array<TType> {
|
||||
// All combined @tag directive in all subgraphs per schema coordinate
|
||||
const tagRegister = buildSchemaCoordinateTagRegister(subgraphs.map(s => s.typeDefs));
|
||||
|
||||
let filteredSubgraphs = subgraphs.map(subgraph => {
|
||||
return {
|
||||
...subgraph,
|
||||
...applyTagFilterToInaccessibleTransformOnSubgraphSchema(
|
||||
subgraph.typeDefs,
|
||||
tagRegister,
|
||||
filter,
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
const intersectionOfTypesWhereAllFieldsAreInaccessible = new Set<string>();
|
||||
// We need to traverse all subgraphs to find the intersection of types where all fields are inaccessible.
|
||||
// If a type is not present in any other subgraph, we can safely mark it as inaccessible.
|
||||
filteredSubgraphs.forEach(subgraph => {
|
||||
const otherSubgraphs = filteredSubgraphs.filter(sub => sub !== subgraph);
|
||||
|
||||
for (const [type, allFieldsInaccessible] of subgraph.typesWithAllFieldsInaccessible) {
|
||||
if (
|
||||
allFieldsInaccessible &&
|
||||
otherSubgraphs.every(
|
||||
sub =>
|
||||
!sub.typesWithAllFieldsInaccessible.has(type) ||
|
||||
sub.typesWithAllFieldsInaccessible.get(type) === true,
|
||||
)
|
||||
) {
|
||||
intersectionOfTypesWhereAllFieldsAreInaccessible.add(type);
|
||||
}
|
||||
// let's not visit this type a second time...
|
||||
otherSubgraphs.forEach(sub => {
|
||||
sub.typesWithAllFieldsInaccessible.delete(type);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (!intersectionOfTypesWhereAllFieldsAreInaccessible.size) {
|
||||
return filteredSubgraphs;
|
||||
}
|
||||
|
||||
return filteredSubgraphs.map(subgraph => ({
|
||||
...subgraph,
|
||||
typeDefs: makeTypesFromSetInaccessible(
|
||||
subgraph.typeDefs,
|
||||
/** We exclude the types that are already marked as inaccessible within the subgraph as we want to avoid `@inaccessible` applied more than once. */
|
||||
difference(
|
||||
intersectionOfTypesWhereAllFieldsAreInaccessible,
|
||||
subgraph.typesWithInaccessibleApplied,
|
||||
),
|
||||
subgraph.transformTagDirectives,
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
function difference<$Type>(set1: Set<$Type>, set2: Set<$Type>): Set<$Type> {
|
||||
const result = new Set<$Type>();
|
||||
set1.forEach(item => {
|
||||
if (!set2.has(item)) {
|
||||
result.add(item);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export const extractTagsFromDocument = (
|
||||
documentNode: DocumentNode,
|
||||
tagStrategy: TagExtractionStrategy,
|
||||
|
|
@ -684,8 +209,3 @@ export function createTagDirectiveNameExtractionStrategy(
|
|||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
export type Federation2SubgraphDocumentNodeByTagsFilter = {
|
||||
include: Set<string>;
|
||||
exclude: Set<string>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,160 +0,0 @@
|
|||
import {
|
||||
buildASTSchema,
|
||||
getNamedType,
|
||||
isEnumType,
|
||||
isInputObjectType,
|
||||
isInterfaceType,
|
||||
isObjectType,
|
||||
isScalarType,
|
||||
isUnionType,
|
||||
Kind,
|
||||
specifiedScalarTypes,
|
||||
visit,
|
||||
type ConstDirectiveNode,
|
||||
type DocumentNode,
|
||||
type EnumTypeDefinitionNode,
|
||||
type GraphQLNamedType,
|
||||
type GraphQLType,
|
||||
type InputObjectTypeDefinitionNode,
|
||||
type InterfaceTypeDefinitionNode,
|
||||
type ObjectTypeDefinitionNode,
|
||||
type ScalarTypeDefinitionNode,
|
||||
type UnionTypeDefinitionNode,
|
||||
} from 'graphql';
|
||||
|
||||
const specifiedScalarNames = new Set(specifiedScalarTypes.map(t => t.name));
|
||||
|
||||
/**
|
||||
* Retrieve a named list of all types that are reachable from the root types.
|
||||
*/
|
||||
export function getReachableTypes(documentNode: DocumentNode): Set<string> {
|
||||
const reachableTypeNames = new Set<string>();
|
||||
const schema = buildASTSchema(documentNode);
|
||||
const didVisitType = new Set<GraphQLType>();
|
||||
const typeQueue: Array<GraphQLNamedType> = [];
|
||||
|
||||
const queryType = schema.getQueryType();
|
||||
const mutationType = schema.getMutationType();
|
||||
const subscriptionType = schema.getSubscriptionType();
|
||||
|
||||
if (queryType) {
|
||||
processNamedType(queryType);
|
||||
}
|
||||
if (mutationType) {
|
||||
processNamedType(mutationType);
|
||||
}
|
||||
if (subscriptionType) {
|
||||
processNamedType(subscriptionType);
|
||||
}
|
||||
|
||||
function processNamedType(tType: GraphQLNamedType) {
|
||||
if (didVisitType.has(tType) || specifiedScalarNames.has(tType.name)) {
|
||||
return;
|
||||
}
|
||||
didVisitType.add(tType);
|
||||
typeQueue.push(tType);
|
||||
reachableTypeNames.add(tType.name);
|
||||
}
|
||||
|
||||
let currentType: GraphQLNamedType | undefined;
|
||||
|
||||
while ((currentType = typeQueue.shift())) {
|
||||
if (isObjectType(currentType)) {
|
||||
for (const field of Object.values(currentType.getFields())) {
|
||||
const fieldType = getNamedType(field.type);
|
||||
processNamedType(fieldType);
|
||||
|
||||
for (const arg of field.args) {
|
||||
const argType = getNamedType(arg.type);
|
||||
processNamedType(argType);
|
||||
}
|
||||
}
|
||||
currentType.getInterfaces().forEach(processNamedType);
|
||||
} else if (isInputObjectType(currentType)) {
|
||||
for (const field of Object.values(currentType.getFields())) {
|
||||
const fieldType = getNamedType(field.type);
|
||||
processNamedType(fieldType);
|
||||
}
|
||||
} else if (isScalarType(currentType) || isEnumType(currentType)) {
|
||||
reachableTypeNames.add(currentType.name);
|
||||
} else if (isInterfaceType(currentType)) {
|
||||
for (const field of Object.values(currentType.getFields())) {
|
||||
const fieldType = getNamedType(field.type);
|
||||
processNamedType(fieldType);
|
||||
}
|
||||
currentType.getInterfaces().forEach(processNamedType);
|
||||
schema.getPossibleTypes(currentType).forEach(processNamedType);
|
||||
} else if (isUnionType(currentType)) {
|
||||
currentType.getTypes().forEach(processNamedType);
|
||||
}
|
||||
}
|
||||
|
||||
return reachableTypeNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a function that adds a directive with a given name to a list of directives if it does not exist yet.
|
||||
*/
|
||||
function createAddDirectiveIfNotExists(directiveName: string) {
|
||||
return function addDirectiveIfNotExists(
|
||||
directives?: readonly ConstDirectiveNode[],
|
||||
): readonly ConstDirectiveNode[] | void {
|
||||
const hasInaccessibleDirective = !!directives?.some(
|
||||
directive => directive.name.value === directiveName,
|
||||
);
|
||||
|
||||
if (!hasInaccessibleDirective) {
|
||||
return [
|
||||
...(directives ?? []),
|
||||
{
|
||||
kind: Kind.DIRECTIVE,
|
||||
name: {
|
||||
kind: Kind.NAME,
|
||||
value: directiveName,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
return directives;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* For a given GraphQL schema document node, add a directive with a given name to all types not in the provided set.
|
||||
*/
|
||||
export function addDirectiveOnTypes(args: {
|
||||
documentNode: DocumentNode;
|
||||
/** a set of excluded types. e.g. as retrieved from the `getReachableTypes` function. */
|
||||
excludedTypeNames: Set<string>;
|
||||
/** name of the directive that should be added on the types. */
|
||||
directiveName: string;
|
||||
}): DocumentNode {
|
||||
const addDirectiveIfNotExists = createAddDirectiveIfNotExists(args.directiveName);
|
||||
|
||||
function onNamedTypeDefinitionNode(
|
||||
node:
|
||||
| ObjectTypeDefinitionNode
|
||||
| UnionTypeDefinitionNode
|
||||
| InterfaceTypeDefinitionNode
|
||||
| ScalarTypeDefinitionNode
|
||||
| EnumTypeDefinitionNode
|
||||
| InputObjectTypeDefinitionNode,
|
||||
) {
|
||||
if (args.excludedTypeNames.has(node.name.value)) {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
...node,
|
||||
directives: addDirectiveIfNotExists(node.directives),
|
||||
};
|
||||
}
|
||||
|
||||
return visit(args.documentNode, {
|
||||
[Kind.OBJECT_TYPE_DEFINITION]: onNamedTypeDefinitionNode,
|
||||
[Kind.INTERFACE_TYPE_DEFINITION]: onNamedTypeDefinitionNode,
|
||||
[Kind.UNION_TYPE_DEFINITION]: onNamedTypeDefinitionNode,
|
||||
[Kind.ENUM_TYPE_DEFINITION]: onNamedTypeDefinitionNode,
|
||||
[Kind.INPUT_OBJECT_TYPE_DEFINITION]: onNamedTypeDefinitionNode,
|
||||
[Kind.SCALAR_TYPE_DEFINITION]: onNamedTypeDefinitionNode,
|
||||
});
|
||||
}
|
||||
|
|
@ -33,6 +33,7 @@
|
|||
"@sentry/integrations": "7.114.0",
|
||||
"@sentry/node": "7.120.2",
|
||||
"@swc/core": "1.13.5",
|
||||
"@theguild/federation-composition": "0.20.0",
|
||||
"@trpc/client": "10.45.2",
|
||||
"@trpc/server": "10.45.2",
|
||||
"@whatwg-node/server": "0.10.5",
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@ import { parse, type DocumentNode } from 'graphql';
|
|||
import { createSchema } from 'graphql-yoga';
|
||||
import { mergeTypeDefs } from '@graphql-tools/merge';
|
||||
import { type Registry } from '@hive/api';
|
||||
import { composeFederationV2 } from '@hive/schema/src/lib/compose';
|
||||
import { applyTagFilterOnSubgraphs } from '@hive/schema/src/lib/federation-tag-extraction';
|
||||
import { composeSchemaContract } from '@theguild/federation-composition';
|
||||
|
||||
/**
|
||||
* Creates the public GraphQL schema from the private GraphQL Schema Registry definition.
|
||||
|
|
@ -14,8 +13,7 @@ export function createPublicGraphQLSchema<TContext>(registry: Registry) {
|
|||
throwOnConflict: true,
|
||||
});
|
||||
|
||||
// Use our tag filter logic for marking everything not tagged with `@tag(name: "public")` as @inaccessible
|
||||
const [filteredSubgraph] = applyTagFilterOnSubgraphs(
|
||||
const compositionResult = composeSchemaContract(
|
||||
[
|
||||
{
|
||||
name: 'public',
|
||||
|
|
@ -26,26 +24,18 @@ export function createPublicGraphQLSchema<TContext>(registry: Registry) {
|
|||
include: new Set(['public']),
|
||||
exclude: new Set(),
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
// Compose the filtered subgraph in order to receive the public schema SDL
|
||||
const compositionResult = composeFederationV2([
|
||||
{
|
||||
typeDefs: filteredSubgraph.typeDefs,
|
||||
name: 'server',
|
||||
url: undefined,
|
||||
},
|
||||
]);
|
||||
|
||||
if (compositionResult.type === 'failure') {
|
||||
if (compositionResult.errors) {
|
||||
throw new Error(
|
||||
'Could not create public GraphQL schema.\nEncountered the following composition errors:\n' +
|
||||
compositionResult.result.errors.map(error => `- ${error.message}`).join('\n'),
|
||||
compositionResult.errors.map(error => `- ${error.message}`).join('\n'),
|
||||
);
|
||||
}
|
||||
|
||||
return createSchema<TContext>({
|
||||
typeDefs: parse(compositionResult.result.sdl),
|
||||
typeDefs: parse(compositionResult.publicSdl),
|
||||
resolvers: registry.resolvers,
|
||||
resolverValidationOptions: {
|
||||
// The resolvers still contain the ones of the public schema
|
||||
|
|
|
|||
|
|
@ -140,6 +140,9 @@ importers:
|
|||
'@theguild/eslint-config':
|
||||
specifier: 0.12.1
|
||||
version: 0.12.1(eslint@8.57.1(patch_hash=08d9d41d21638cb74d0f9f34877a8839601a4e5a8263066ff23e7032addbcba0))(typescript@5.7.3)
|
||||
'@theguild/federation-composition':
|
||||
specifier: 0.20.0
|
||||
version: 0.20.0(graphql@16.9.0)
|
||||
'@theguild/prettier-config':
|
||||
specifier: 2.0.7
|
||||
version: 2.0.7(prettier@3.4.2)
|
||||
|
|
@ -316,8 +319,8 @@ importers:
|
|||
specifier: workspace:*
|
||||
version: link:../packages/services/storage
|
||||
'@theguild/federation-composition':
|
||||
specifier: 0.19.1
|
||||
version: 0.19.1(graphql@16.9.0)
|
||||
specifier: 0.20.0
|
||||
version: 0.20.0(graphql@16.9.0)
|
||||
'@trpc/client':
|
||||
specifier: 10.45.2
|
||||
version: 10.45.2(@trpc/server@10.45.2)
|
||||
|
|
@ -453,8 +456,8 @@ importers:
|
|||
specifier: 4.2.13
|
||||
version: 4.2.13
|
||||
'@theguild/federation-composition':
|
||||
specifier: 0.19.1
|
||||
version: 0.19.1(graphql@16.9.0)
|
||||
specifier: 0.20.0
|
||||
version: 0.20.0(graphql@16.9.0)
|
||||
colors:
|
||||
specifier: 1.4.0
|
||||
version: 1.4.0
|
||||
|
|
@ -764,8 +767,8 @@ importers:
|
|||
specifier: 7.8.0
|
||||
version: 7.8.0
|
||||
'@theguild/federation-composition':
|
||||
specifier: 0.19.1
|
||||
version: 0.19.1(graphql@16.9.0)
|
||||
specifier: 0.20.0
|
||||
version: 0.20.0(graphql@16.9.0)
|
||||
'@trpc/client':
|
||||
specifier: 10.45.2
|
||||
version: 10.45.2(@trpc/server@10.45.2)
|
||||
|
|
@ -1161,8 +1164,8 @@ importers:
|
|||
specifier: 7.120.2
|
||||
version: 7.120.2
|
||||
'@theguild/federation-composition':
|
||||
specifier: 0.19.1
|
||||
version: 0.19.1(graphql@16.9.0)
|
||||
specifier: 0.20.0
|
||||
version: 0.20.0(graphql@16.9.0)
|
||||
'@trpc/server':
|
||||
specifier: 10.45.2
|
||||
version: 10.45.2
|
||||
|
|
@ -1283,6 +1286,9 @@ importers:
|
|||
'@swc/core':
|
||||
specifier: 1.13.5
|
||||
version: 1.13.5
|
||||
'@theguild/federation-composition':
|
||||
specifier: 0.20.0
|
||||
version: 0.20.0(graphql@16.9.0)
|
||||
'@trpc/client':
|
||||
specifier: 10.45.2
|
||||
version: 10.45.2(@trpc/server@10.45.2)
|
||||
|
|
@ -3617,6 +3623,7 @@ packages:
|
|||
|
||||
'@fastify/vite@6.0.7':
|
||||
resolution: {integrity: sha512-+dRo9KUkvmbqdmBskG02SwigWl06Mwkw8SBDK1zTNH6vd4DyXbRvI7RmJEmBkLouSU81KTzy1+OzwHSffqSD6w==}
|
||||
bundledDependencies: []
|
||||
|
||||
'@floating-ui/core@1.2.6':
|
||||
resolution: {integrity: sha512-EvYTiXet5XqweYGClEmpu3BoxmsQ4hkj3QaYA6qEnigCWffTP3vNRwBReTdrwDwo7OoJ3wM8Uoe9Uk4n+d4hfg==}
|
||||
|
|
@ -7786,6 +7793,12 @@ packages:
|
|||
peerDependencies:
|
||||
graphql: ^16.0.0
|
||||
|
||||
'@theguild/federation-composition@0.20.0':
|
||||
resolution: {integrity: sha512-vokfiXP3L0Iba5a4qFYVHXxxi/v1AiIVCGE9xJFbWAFCtUGrRUKz49vFsDqRLdLmFqa3gLcMjVXVN7RsOYTeXg==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
graphql: ^16.0.0
|
||||
|
||||
'@theguild/prettier-config@2.0.7':
|
||||
resolution: {integrity: sha512-FqpgGAaAFbYHFQmkWEZjIhqmk+Oow82/t+0k408qoBd9RsB4QTwSQSDDbNSgFa/K7c8Dcwau5z3XbHUR/ksKqw==}
|
||||
peerDependencies:
|
||||
|
|
@ -8845,11 +8858,6 @@ packages:
|
|||
browser-tabs-lock@1.3.0:
|
||||
resolution: {integrity: sha512-g6nHaobTiT0eMZ7jh16YpD2kcjAp+PInbiVq3M1x6KKaEIVhT4v9oURNIpZLOZ3LQbQ3XYfNhMAb/9hzNLIWrw==}
|
||||
|
||||
browserslist@4.24.3:
|
||||
resolution: {integrity: sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==}
|
||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||
hasBin: true
|
||||
|
||||
browserslist@4.26.0:
|
||||
resolution: {integrity: sha512-P9go2WrP9FiPwLv3zqRD/Uoxo0RSHjzFCiQz7d4vbmwNqQFo9T9WCeP/Qn5EbcKQY6DBbkxEXNcpJOmncNrb7A==}
|
||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||
|
|
@ -9960,9 +9968,6 @@ packages:
|
|||
electron-to-chromium@1.5.218:
|
||||
resolution: {integrity: sha512-uwwdN0TUHs8u6iRgN8vKeWZMRll4gBkz+QMqdS7DDe49uiK68/UX92lFb61oiFPrpYZNeZIqa4bA7O6Aiasnzg==}
|
||||
|
||||
electron-to-chromium@1.5.76:
|
||||
resolution: {integrity: sha512-CjVQyG7n7Sr+eBXE86HIulnL5N8xZY1sgmOPGuq/F0Rr0FJq63lg0kEtOIDfZBk44FnDLf6FUJ+dsJcuiUDdDQ==}
|
||||
|
||||
emoji-regex@10.3.0:
|
||||
resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==}
|
||||
|
||||
|
|
@ -13080,9 +13085,6 @@ packages:
|
|||
node-int64@0.4.0:
|
||||
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
|
||||
|
||||
node-releases@2.0.19:
|
||||
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
|
||||
|
||||
node-releases@2.0.21:
|
||||
resolution: {integrity: sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==}
|
||||
|
||||
|
|
@ -15776,12 +15778,6 @@ packages:
|
|||
resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
update-browserslist-db@1.1.1:
|
||||
resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
browserslist: '>= 4.21.0'
|
||||
|
||||
update-browserslist-db@1.1.3:
|
||||
resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==}
|
||||
hasBin: true
|
||||
|
|
@ -17709,7 +17705,7 @@ snapshots:
|
|||
dependencies:
|
||||
'@babel/compat-data': 7.26.3
|
||||
'@babel/helper-validator-option': 7.25.9
|
||||
browserslist: 4.24.3
|
||||
browserslist: 4.26.0
|
||||
lru-cache: 5.1.1
|
||||
semver: 6.3.1
|
||||
|
||||
|
|
@ -24768,6 +24764,16 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@theguild/federation-composition@0.20.0(graphql@16.9.0)':
|
||||
dependencies:
|
||||
constant-case: 3.0.4
|
||||
debug: 4.4.1(supports-color@8.1.1)
|
||||
graphql: 16.9.0
|
||||
json5: 2.2.3
|
||||
lodash.sortby: 4.7.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@theguild/prettier-config@2.0.7(prettier@3.4.2)':
|
||||
dependencies:
|
||||
'@ianvs/prettier-plugin-sort-imports': 4.3.1(prettier@3.4.2)
|
||||
|
|
@ -25991,13 +25997,6 @@ snapshots:
|
|||
dependencies:
|
||||
lodash: 4.17.21
|
||||
|
||||
browserslist@4.24.3:
|
||||
dependencies:
|
||||
caniuse-lite: 1.0.30001690
|
||||
electron-to-chromium: 1.5.76
|
||||
node-releases: 2.0.19
|
||||
update-browserslist-db: 1.1.1(browserslist@4.24.3)
|
||||
|
||||
browserslist@4.26.0:
|
||||
dependencies:
|
||||
baseline-browser-mapping: 2.8.3
|
||||
|
|
@ -26561,7 +26560,7 @@ snapshots:
|
|||
|
||||
core-js-compat@3.37.1:
|
||||
dependencies:
|
||||
browserslist: 4.24.3
|
||||
browserslist: 4.26.0
|
||||
|
||||
core-js-pure@3.37.1: {}
|
||||
|
||||
|
|
@ -27233,8 +27232,6 @@ snapshots:
|
|||
|
||||
electron-to-chromium@1.5.218: {}
|
||||
|
||||
electron-to-chromium@1.5.76: {}
|
||||
|
||||
emoji-regex@10.3.0: {}
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
|
|
@ -31382,8 +31379,6 @@ snapshots:
|
|||
|
||||
node-int64@0.4.0: {}
|
||||
|
||||
node-releases@2.0.19: {}
|
||||
|
||||
node-releases@2.0.21: {}
|
||||
|
||||
node-sql-parser@4.12.0:
|
||||
|
|
@ -34401,12 +34396,6 @@ snapshots:
|
|||
|
||||
upath@1.2.0: {}
|
||||
|
||||
update-browserslist-db@1.1.1(browserslist@4.24.3):
|
||||
dependencies:
|
||||
browserslist: 4.24.3
|
||||
escalade: 3.2.0
|
||||
picocolors: 1.1.1
|
||||
|
||||
update-browserslist-db@1.1.3(browserslist@4.26.0):
|
||||
dependencies:
|
||||
browserslist: 4.26.0
|
||||
|
|
|
|||
Loading…
Reference in a new issue