From 769161183e6855a1d129570c0337270a0fc29b0f Mon Sep 17 00:00:00 2001
From: Emil Eriksson
Date: Mon, 6 Oct 2025 08:48:12 +0200
Subject: [PATCH 1/6] Added Apollo Federation V2 support
---
packages/commands/commands/src/index.ts | 1 +
packages/commands/coverage/src/index.ts | 2 +
.../validate/apollo-federation-v2.test.ts | 449 ++++++++++++++++++
packages/core/__tests__/validate/aws.test.ts | 1 +
packages/loaders/loaders/src/index.ts | 104 +++-
5 files changed, 554 insertions(+), 3 deletions(-)
create mode 100644 packages/core/__tests__/validate/apollo-federation-v2.test.ts
diff --git a/packages/commands/commands/src/index.ts b/packages/commands/commands/src/index.ts
index be6dce25c0..f141c1906c 100644
--- a/packages/commands/commands/src/index.ts
+++ b/packages/commands/commands/src/index.ts
@@ -38,6 +38,7 @@ export interface GlobalArgs {
leftHeader?: string[];
rightHeader?: string[];
federation?: boolean;
+ federationV2?: boolean;
aws?: boolean;
method?: string;
}
diff --git a/packages/commands/coverage/src/index.ts b/packages/commands/coverage/src/index.ts
index fef400cbfa..a7f1d1fcfc 100644
--- a/packages/commands/coverage/src/index.ts
+++ b/packages/commands/coverage/src/index.ts
@@ -106,6 +106,7 @@ export default createCommand<
const silent = args.silent;
const { headers, token } = parseGlobalArgs(args);
const apolloFederation = args.federation || false;
+ const apolloFederationV2 = args.federationV2 || false;
const aws = args.aws || false;
const method = args.method?.toUpperCase() || 'POST';
@@ -117,6 +118,7 @@ export default createCommand<
method,
},
apolloFederation,
+ apolloFederationV2,
aws,
);
const documents = await loaders.loadDocuments(args.documents);
diff --git a/packages/core/__tests__/validate/apollo-federation-v2.test.ts b/packages/core/__tests__/validate/apollo-federation-v2.test.ts
new file mode 100644
index 0000000000..150f63c759
--- /dev/null
+++ b/packages/core/__tests__/validate/apollo-federation-v2.test.ts
@@ -0,0 +1,449 @@
+import { parse, print, Source } from 'graphql';
+import { LoadersRegistry } from '@graphql-inspector/loaders';
+import { validate } from '../../src/index.js';
+
+describe('apollo federation v2', () => {
+ test('should accept basic Federation V2 directives', async () => {
+ const doc = parse(/* GraphQL */ `
+ query getUser {
+ user(id: "1") {
+ id
+ name
+ email
+ orders {
+ id
+ total
+ }
+ }
+ }
+ `);
+
+ const schema = await new LoadersRegistry().loadSchema(
+ /* GraphQL */ `
+ type User @key(fields: "id") {
+ id: ID!
+ name: String!
+ email: String!
+ orders: [Order!]! @external
+ }
+
+ type Order @key(fields: "id") {
+ id: ID!
+ total: Float!
+ user: User! @external
+ }
+
+ type Query {
+ user(id: ID!): User
+ }
+ `,
+ {},
+ false,
+ true,
+ false,
+ );
+
+ const results = validate(schema, [new Source(print(doc))]);
+
+ expect(results).toHaveLength(0);
+ });
+
+ test('should accept @shareable directive on fields', async () => {
+ const doc = parse(/* GraphQL */ `
+ query getProduct {
+ product(id: "1") {
+ id
+ name
+ description
+ }
+ }
+ `);
+
+ const schema = await new LoadersRegistry().loadSchema(
+ /* GraphQL */ `
+ extend schema
+ @link(
+ url: "https://specs.apollographql.com/federation/v2.3"
+ import: ["@key", "@shareable"]
+ )
+
+ type Product @key(fields: "id") {
+ id: ID!
+ name: String! @shareable
+ description: String @shareable
+ }
+
+ type Query {
+ product(id: ID!): Product
+ }
+ `,
+ {},
+ false,
+ true,
+ false,
+ );
+
+ const results = validate(schema, [new Source(print(doc))]);
+
+ expect(results).toHaveLength(0);
+ });
+
+ test('should accept @requires and @provides directives', async () => {
+ const doc = parse(/* GraphQL */ `
+ query getUserWithShippingEstimate {
+ user(id: "1") {
+ id
+ shippingEstimate
+ cart {
+ estimatedDelivery
+ }
+ }
+ }
+ `);
+
+ const schema = await new LoadersRegistry().loadSchema(
+ /* GraphQL */ `
+ extend schema
+ @link(
+ url: "https://specs.apollographql.com/federation/v2.3"
+ import: ["@key", "@external", "@requires", "@provides"]
+ )
+
+ type User @key(fields: "id") {
+ id: ID!
+ zipCode: String @external
+ shippingEstimate: String @requires(fields: "zipCode")
+ cart: Cart @provides(fields: "estimatedDelivery")
+ }
+
+ type Cart {
+ estimatedDelivery: String @external
+ }
+
+ type Query {
+ user(id: ID!): User
+ }
+ `,
+ {},
+ false,
+ true,
+ false,
+ );
+
+ const results = validate(schema, [new Source(print(doc))]);
+
+ expect(results).toHaveLength(0);
+ });
+
+ test('should accept @override directive', async () => {
+ const doc = parse(/* GraphQL */ `
+ query getAccount {
+ account(id: "1") {
+ id
+ balance
+ currency
+ }
+ }
+ `);
+
+ const schema = await new LoadersRegistry().loadSchema(
+ /* GraphQL */ `
+ extend schema
+ @link(
+ url: "https://specs.apollographql.com/federation/v2.3"
+ import: ["@key", "@override"]
+ )
+
+ type Account @key(fields: "id") {
+ id: ID!
+ balance: Float! @override(from: "accounts")
+ currency: String!
+ }
+
+ type Query {
+ account(id: ID!): Account
+ }
+ `,
+ {},
+ false,
+ true,
+ false,
+ );
+
+ const results = validate(schema, [new Source(print(doc))]);
+
+ expect(results).toHaveLength(0);
+ });
+
+ test('should accept @inaccessible directive', async () => {
+ const doc = parse(/* GraphQL */ `
+ query getUser {
+ user(id: "1") {
+ id
+ name
+ }
+ }
+ `);
+
+ const schema = await new LoadersRegistry().loadSchema(
+ /* GraphQL */ `
+ extend schema
+ @link(
+ url: "https://specs.apollographql.com/federation/v2.3"
+ import: ["@key", "@inaccessible"]
+ )
+
+ type User @key(fields: "id") {
+ id: ID!
+ name: String!
+ internalId: String @inaccessible
+ }
+
+ type Query {
+ user(id: ID!): User
+ }
+ `,
+ {},
+ false,
+ true,
+ false,
+ );
+
+ const results = validate(schema, [new Source(print(doc))]);
+
+ expect(results).toHaveLength(0);
+ });
+
+ test('should accept @tag directive', async () => {
+ const doc = parse(/* GraphQL */ `
+ query getProduct {
+ product(id: "1") {
+ id
+ name
+ price
+ }
+ }
+ `);
+
+ const schema = await new LoadersRegistry().loadSchema(
+ /* GraphQL */ `
+ extend schema
+ @link(url: "https://specs.apollographql.com/federation/v2.3", import: ["@key", "@tag"])
+
+ type Product @key(fields: "id") {
+ id: ID!
+ name: String!
+ price: Float! @tag(name: "pricing")
+ }
+
+ type Query {
+ product(id: ID!): Product
+ }
+ `,
+ {},
+ false,
+ true,
+ false,
+ );
+
+ const results = validate(schema, [new Source(print(doc))]);
+
+ expect(results).toHaveLength(0);
+ });
+
+ test('should accept @interfaceObject directive', async () => {
+ const doc = parse(/* GraphQL */ `
+ query getEntity {
+ entity(id: "1") {
+ id
+ name
+ }
+ }
+ `);
+
+ const schema = await new LoadersRegistry().loadSchema(
+ /* GraphQL */ `
+ extend schema
+ @link(
+ url: "https://specs.apollographql.com/federation/v2.3"
+ import: ["@key", "@interfaceObject"]
+ )
+
+ type Entity @key(fields: "id") @interfaceObject {
+ id: ID!
+ name: String!
+ }
+
+ type Query {
+ entity(id: ID!): Entity
+ }
+ `,
+ {},
+ false,
+ true,
+ false,
+ );
+
+ const results = validate(schema, [new Source(print(doc))]);
+
+ expect(results).toHaveLength(0);
+ });
+
+ test('should accept @composeDirective directive', async () => {
+ const doc = parse(/* GraphQL */ `
+ query getUser {
+ user(id: "1") {
+ id
+ name
+ profile
+ }
+ }
+ `);
+
+ const schema = await new LoadersRegistry().loadSchema(
+ /* GraphQL */ `
+ extend schema
+ @link(
+ url: "https://specs.apollographql.com/federation/v2.3"
+ import: ["@key", "@composeDirective"]
+ )
+ @composeDirective(name: "@custom")
+
+ directive @custom(value: String) on FIELD_DEFINITION
+
+ type User @key(fields: "id") {
+ id: ID!
+ name: String!
+ profile: String @custom(value: "user-profile")
+ }
+
+ type Query {
+ user(id: ID!): User
+ }
+ `,
+ {},
+ false,
+ true,
+ false,
+ );
+
+ const results = validate(schema, [new Source(print(doc))]);
+
+ expect(results).toHaveLength(0);
+ });
+
+ test('should accept complex Federation V2 schema with multiple entities', async () => {
+ const doc = parse(/* GraphQL */ `
+ query getRecommendations {
+ user(id: "1") {
+ id
+ name
+ recommendations {
+ id
+ name
+ price
+ reviews {
+ id
+ rating
+ comment
+ }
+ }
+ }
+ }
+ `);
+
+ const schema = await new LoadersRegistry().loadSchema(
+ /* GraphQL */ `
+ extend schema
+ @link(
+ url: "https://specs.apollographql.com/federation/v2.3"
+ import: ["@key", "@external", "@requires", "@provides", "@shareable", "@tag"]
+ )
+
+ type User @key(fields: "id") {
+ id: ID!
+ name: String! @shareable
+ age: Int @external
+ recommendations: [Product!]!
+ @requires(fields: "age")
+ @provides(fields: "reviews { rating }")
+ }
+
+ type Product @key(fields: "id") {
+ id: ID!
+ name: String! @shareable
+ price: Float! @tag(name: "pricing")
+ reviews: [Review!]! @external
+ }
+
+ type Review @key(fields: "id") {
+ id: ID!
+ rating: Int @external
+ comment: String
+ }
+
+ type Query {
+ user(id: ID!): User
+ }
+ `,
+ {},
+ false,
+ true,
+ false,
+ );
+
+ const results = validate(schema, [new Source(print(doc))]);
+
+ expect(results).toHaveLength(0);
+ });
+
+ test('should handle Federation V2 with custom scalar types', async () => {
+ const doc = parse(/* GraphQL */ `
+ query getEvent {
+ event(id: "1") {
+ id
+ timestamp
+ location {
+ coordinates
+ }
+ }
+ }
+ `);
+
+ const schema = await new LoadersRegistry().loadSchema(
+ /* GraphQL */ `
+ extend schema
+ @link(
+ url: "https://specs.apollographql.com/federation/v2.3"
+ import: ["@key", "@shareable"]
+ )
+
+ scalar DateTime
+ scalar GeoCoordinates
+
+ type Event @key(fields: "id") {
+ id: ID!
+ timestamp: DateTime! @shareable
+ location: Location
+ }
+
+ type Location @key(fields: "id") {
+ id: ID!
+ coordinates: GeoCoordinates!
+ }
+
+ type Query {
+ event(id: ID!): Event
+ }
+ `,
+ {},
+ false,
+ true,
+ false,
+ );
+
+ const results = validate(schema, [new Source(print(doc))]);
+
+ expect(results).toHaveLength(0);
+ });
+});
diff --git a/packages/core/__tests__/validate/aws.test.ts b/packages/core/__tests__/validate/aws.test.ts
index 8ece72b220..7bd4d88fae 100644
--- a/packages/core/__tests__/validate/aws.test.ts
+++ b/packages/core/__tests__/validate/aws.test.ts
@@ -42,6 +42,7 @@ describe('aws', () => {
`,
{},
false,
+ false,
true,
);
diff --git a/packages/loaders/loaders/src/index.ts b/packages/loaders/loaders/src/index.ts
index 60208c6cba..66dd6e7716 100644
--- a/packages/loaders/loaders/src/index.ts
+++ b/packages/loaders/loaders/src/index.ts
@@ -1,4 +1,4 @@
-import { buildSchema, GraphQLSchema } from 'graphql';
+import { buildSchema, GraphQLSchema, isInterfaceType, isObjectType, printSchema } from 'graphql';
import { InspectorConfig } from '@graphql-inspector/config';
import {
loadDocuments,
@@ -26,16 +26,81 @@ export class LoadersRegistry {
}
}
- loadSchema(
+ async loadSchema(
pointer: string,
options: Omit = {},
enableApolloFederation: boolean,
+ enableApolloFederationV2: boolean,
enableAWS: boolean,
): Promise {
- return enrichError(
+ const schema = await enrichError(
loadSchema(pointer, {
loaders: this.loaders,
...options,
+ ...(enableApolloFederationV2
+ ? {
+ schemas: [
+ buildSchema(/* GraphQL */ `
+ scalar _Any
+ union _Entity
+ scalar FieldSet
+ scalar link__Import
+ scalar federation__ContextFieldValue
+ scalar federation__Scope
+ scalar federation__Policy
+ type Query
+
+ enum link__Purpose {
+ SECURITY
+ EXECUTION
+ }
+
+ type _Service {
+ sdl: String!
+ }
+
+ extend type Query {
+ _entities(representations: [_Any!]!): [_Entity]!
+ _service: _Service!
+ }
+
+ directive @external on FIELD_DEFINITION | OBJECT
+ directive @requires(fields: FieldSet!) on FIELD_DEFINITION
+ directive @provides(fields: FieldSet!) on FIELD_DEFINITION
+ directive @key(
+ fields: FieldSet!
+ resolvable: Boolean = true
+ ) repeatable on OBJECT | INTERFACE
+ directive @link(
+ url: String!
+ as: String
+ for: link__Purpose
+ import: [link__Import]
+ ) repeatable on SCHEMA
+ directive @shareable repeatable on OBJECT | FIELD_DEFINITION
+ directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
+ directive @tag(
+ name: String!
+ ) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
+ directive @override(from: String!) on FIELD_DEFINITION
+ directive @composeDirective(name: String!) repeatable on SCHEMA
+ directive @interfaceObject on OBJECT
+ directive @authenticated on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
+ directive @requiresScopes(
+ scopes: [[federation__Scope!]!]!
+ ) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
+ directive @policy(
+ policies: [[federation__Policy!]!]!
+ ) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
+ directive @context(name: String!) repeatable on INTERFACE | OBJECT | UNION
+ directive @fromContext(
+ field: federation__ContextFieldValue
+ ) on ARGUMENT_DEFINITION
+ directive @extends on OBJECT | INTERFACE
+ `),
+ ],
+ }
+ : {}),
...(enableApolloFederation
? {
schemas: [
@@ -86,6 +151,12 @@ export class LoadersRegistry {
: {}),
}),
);
+
+ if (enableApolloFederationV2) {
+ return this.buildEntityUnion(schema);
+ }
+
+ return schema;
}
loadDocuments(
@@ -99,6 +170,33 @@ export class LoadersRegistry {
}),
);
}
+
+ private buildEntityUnion(schema: GraphQLSchema): GraphQLSchema {
+ const entityTypes: string[] = [];
+ const typeMap = schema.getTypeMap();
+
+ // Find all types with @key directive
+ Object.values(typeMap).forEach(type => {
+ if (
+ (isObjectType(type) || isInterfaceType(type)) &&
+ type.astNode?.directives?.some(dir => dir.name.value === 'key')
+ ) {
+ entityTypes.push(type.name);
+ }
+ });
+
+ if (entityTypes.length === 0) {
+ return schema; // No entities found
+ }
+
+ // Create new schema SDL with populated _Entity union
+ const entityUnion = `union _Entity = ${entityTypes.join(' | ')}`;
+
+ // Rebuild schema with proper _Entity union
+ const schemaSDL = printSchema(schema).replace('union _Entity', entityUnion);
+
+ return buildSchema(schemaSDL);
+ }
}
export type Loaders = Pick;
From 472da44e3eefd8c4c673e8e117c9e32e34a95082 Mon Sep 17 00:00:00 2001
From: Emil Eriksson
Date: Mon, 6 Oct 2025 09:28:53 +0200
Subject: [PATCH 2/6] Added missing arguments
---
packages/commands/diff/src/index.ts | 3 +++
packages/commands/introspect/src/index.ts | 2 ++
packages/commands/serve/src/index.ts | 2 ++
packages/commands/similar/src/index.ts | 2 ++
packages/commands/validate/src/index.ts | 3 +++
5 files changed, 12 insertions(+)
diff --git a/packages/commands/diff/src/index.ts b/packages/commands/diff/src/index.ts
index 61e36af53d..6c63f2b2c5 100644
--- a/packages/commands/diff/src/index.ts
+++ b/packages/commands/diff/src/index.ts
@@ -130,6 +130,7 @@ export default createCommand<
const oldSchemaPointer = args.oldSchema;
const newSchemaPointer = args.newSchema;
const apolloFederation = args.federation || false;
+ const apolloFederationV2 = args.federationV2 || false;
const aws = args.aws || false;
const method = args.method?.toUpperCase() || 'POST';
const { headers, leftHeaders, rightHeaders, token } = parseGlobalArgs(args);
@@ -151,6 +152,7 @@ export default createCommand<
method,
},
apolloFederation,
+ apolloFederationV2,
aws,
);
const newSchema = await loaders.loadSchema(
@@ -161,6 +163,7 @@ export default createCommand<
method,
},
apolloFederation,
+ apolloFederationV2,
aws,
);
diff --git a/packages/commands/introspect/src/index.ts b/packages/commands/introspect/src/index.ts
index 5443c385e0..e9bc44ab68 100644
--- a/packages/commands/introspect/src/index.ts
+++ b/packages/commands/introspect/src/index.ts
@@ -89,6 +89,7 @@ export default createCommand<
const output = args.write!;
const comments = args.comments || false;
const apolloFederation = args.federation || false;
+ const apolloFederationV2 = args.federationV2 || false;
const aws = args.aws || false;
const method = args.method?.toUpperCase() || 'POST';
@@ -100,6 +101,7 @@ export default createCommand<
method,
},
apolloFederation,
+ apolloFederationV2,
aws,
);
diff --git a/packages/commands/serve/src/index.ts b/packages/commands/serve/src/index.ts
index 93d27b8b60..de6b07b9a0 100644
--- a/packages/commands/serve/src/index.ts
+++ b/packages/commands/serve/src/index.ts
@@ -43,6 +43,7 @@ export default createCommand<
async handler(args) {
const { headers, token } = parseGlobalArgs(args);
const apolloFederation = args.federation || false;
+ const apolloFederationV2 = args.federationV2 || false;
const aws = args.aws || false;
const method = args.method?.toUpperCase() || 'POST';
@@ -54,6 +55,7 @@ export default createCommand<
method,
},
apolloFederation,
+ apolloFederationV2,
aws,
);
diff --git a/packages/commands/similar/src/index.ts b/packages/commands/similar/src/index.ts
index 2b7e4d3f7f..1ef9d1b6c7 100644
--- a/packages/commands/similar/src/index.ts
+++ b/packages/commands/similar/src/index.ts
@@ -117,6 +117,7 @@ export default createCommand<
const type = args.name;
const threshold = args.threshold;
const apolloFederation = args.federation || false;
+ const apolloFederationV2 = args.federationV2 || false;
const aws = args.aws || false;
const method = args.method?.toUpperCase() || 'POST';
@@ -128,6 +129,7 @@ export default createCommand<
method,
},
apolloFederation,
+ apolloFederationV2,
aws,
);
diff --git a/packages/commands/validate/src/index.ts b/packages/commands/validate/src/index.ts
index 63e61d134a..cd6b6e5e56 100644
--- a/packages/commands/validate/src/index.ts
+++ b/packages/commands/validate/src/index.ts
@@ -278,6 +278,8 @@ export default createCommand<
const apollo = args.apollo || false;
const aws = args.aws || false;
const apolloFederation = args.federation || false;
+ const apolloFederationV2 = args.federationV2 || false;
+
const method = args.method?.toUpperCase() || 'POST';
const maxDepth = args.maxDepth == null ? undefined : args.maxDepth;
const maxAliasCount = args.maxAliasCount == null ? undefined : args.maxAliasCount;
@@ -311,6 +313,7 @@ export default createCommand<
method,
},
apolloFederation,
+ apolloFederationV2,
aws,
);
const documents = await loaders.loadDocuments(args.documents, {
From c0c42b51b80aa538d3dc094c870ac48a12fb4cb9 Mon Sep 17 00:00:00 2001
From: Emil Eriksson
Date: Tue, 7 Oct 2025 07:58:51 +0200
Subject: [PATCH 3/6] updated documentation
---
website/src/pages/docs/commands/coverage.mdx | 3 ++-
website/src/pages/docs/commands/diff.mdx | 3 ++-
website/src/pages/docs/commands/introspect.mdx | 3 ++-
website/src/pages/docs/commands/serve.mdx | 3 ++-
website/src/pages/docs/commands/similar.mdx | 3 ++-
website/src/pages/docs/commands/validate.mdx | 3 ++-
6 files changed, 12 insertions(+), 6 deletions(-)
diff --git a/website/src/pages/docs/commands/coverage.mdx b/website/src/pages/docs/commands/coverage.mdx
index 89195661ef..32a5796db6 100644
--- a/website/src/pages/docs/commands/coverage.mdx
+++ b/website/src/pages/docs/commands/coverage.mdx
@@ -33,7 +33,8 @@ graphql-inspector coverage DOCUMENTS SCHEMA
- `-t, --token ` - an access token
- `-h, --header ` - set http header (`--header 'Auth: Basic 123'`)
- `--method` - method on url schema pointers (default: `POST`)
-- `--federation` - Support Apollo Federation directives (default: `false`)
+- `--federation` - Support Apollo Federation V1 directives (default: `false`)
+- `--federationV2` - Support Apollo Federation V2 directives (default: `false`)
- `--aws` - Support AWS Appsync directives and scalar types (default: `false`)
## Output
diff --git a/website/src/pages/docs/commands/diff.mdx b/website/src/pages/docs/commands/diff.mdx
index 785a945388..e271fd945f 100644
--- a/website/src/pages/docs/commands/diff.mdx
+++ b/website/src/pages/docs/commands/diff.mdx
@@ -50,7 +50,8 @@ graphql-inspector diff OLD_SCHEMA NEW_SCHEMA
- `-t, --token ` - an access token
- `-h, --header ` - set http header (`--header 'Auth: Basic 123')
- `--method` - method on url schema pointers (default: `POST`)
-- `--federation` - Support Apollo Federation directives (default: `false`)
+- `--federation` - Support Apollo Federation V1 directives (default: `false`)
+- `--federationV2` - Support Apollo Federation V2 directives (default: `false`)
- `--aws` - Support AWS Appsync directives and scalar types (default: `false`)
### Output
diff --git a/website/src/pages/docs/commands/introspect.mdx b/website/src/pages/docs/commands/introspect.mdx
index f826ba52ef..a51997d66c 100644
--- a/website/src/pages/docs/commands/introspect.mdx
+++ b/website/src/pages/docs/commands/introspect.mdx
@@ -29,7 +29,8 @@ It supports `.graphql`, `.gql` and `.json` extensions.
- `-t, --token ` - an access token
- `-h, --header ` - set HTTP header (`--header 'Auth: Basic 123'`)
- `--method` - method on URL schema pointers (default: `POST`)
-- `--federation` - Support Apollo Federation directives (default: `false`)
+- `--federation` - Support Apollo Federation V1 directives (default: `false`)
+- `--federationV2` - Support Apollo Federation V2 directives (default: `false`)
- `--aws` - Support AWS Appsync directives and scalar types (default: `false`)
## Output
diff --git a/website/src/pages/docs/commands/serve.mdx b/website/src/pages/docs/commands/serve.mdx
index 9827c2eba9..e343db3f6a 100644
--- a/website/src/pages/docs/commands/serve.mdx
+++ b/website/src/pages/docs/commands/serve.mdx
@@ -26,7 +26,8 @@ graphql-inspector serve SCHEMA
- `-t, --token ` - an access token
- `-h, --header ` - set HTTP header (`--header 'Auth: Basic 123'`)
- `--method` - method on URL schema pointers (default: `POST`)
-- `--federation` - Support Apollo Federation directives (default: `false`)
+- `--federation` - Support Apollo Federation V1 directives (default: `false`)
+- `--federationV2` - Support Apollo Federation V2 directives (default: `false`)
- `--aws` - Support AWS Appsync directives and scalar types (default: `false`)
## Output
diff --git a/website/src/pages/docs/commands/similar.mdx b/website/src/pages/docs/commands/similar.mdx
index 908808edfd..7dcc2ab6a5 100644
--- a/website/src/pages/docs/commands/similar.mdx
+++ b/website/src/pages/docs/commands/similar.mdx
@@ -29,7 +29,8 @@ graphql-inspector similar SCHEMA
- `-t, --token ` - an access token
- `-h, --header ` - set HTTP header (`--header 'Auth: Basic 123'`)
- `--method` - method on URL schema pointers (default: `POST`)
-- `--federation` - Support Apollo Federation directives (default: `false`)
+- `--federation` - Support Apollo Federation V1 directives (default: `false`)
+- `--federationV2` - Support Apollo Federation V2 directives (default: `false`)
- `--aws` - Support AWS Appsync directives and scalar types (default: `false`)
## Output
diff --git a/website/src/pages/docs/commands/validate.mdx b/website/src/pages/docs/commands/validate.mdx
index 19176b98ab..29b50b2663 100644
--- a/website/src/pages/docs/commands/validate.mdx
+++ b/website/src/pages/docs/commands/validate.mdx
@@ -32,7 +32,8 @@ graphql-inspector validate DOCUMENTS SCHEMA
- `--keepClientFields ` - Keeps the fields with `@client`, but removes `@client` directive from
them - works only with combination of `--apollo` (default: `false`)
- `--method` - method on url schema pointers (default: `POST`)
-- `--federation` - Support Apollo Federation directives (default: `false`)
+- `--federation` - Support Apollo Federation V1 directives (default: `false`)
+- `--federationV2` - Support Apollo Federation V2 directives (default: `false`)
- `--aws` - Support AWS Appsync directives and scalar types (default: `false`)
- `--maxDepth ` - Fail when operation depth exceeds maximum depth (default: `false`)
- `--maxAliasCount ` - Fail when alias count (including the referenced fragments) exceeds maximum
From be5f2628d2a983ef2ddf2f98ea6ffa5ab4fe501e Mon Sep 17 00:00:00 2001
From: Emil Eriksson
Date: Thu, 16 Oct 2025 19:17:50 +0200
Subject: [PATCH 4/6] test to trigger pipeline
---
packages/loaders/loaders/src/index.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/loaders/loaders/src/index.ts b/packages/loaders/loaders/src/index.ts
index 66dd6e7716..0b62561fc8 100644
--- a/packages/loaders/loaders/src/index.ts
+++ b/packages/loaders/loaders/src/index.ts
@@ -48,6 +48,7 @@ export class LoadersRegistry {
scalar federation__ContextFieldValue
scalar federation__Scope
scalar federation__Policy
+
type Query
enum link__Purpose {
From 33ab613c1b278b75390b93cf03ed624d17f01d3e Mon Sep 17 00:00:00 2001
From: Emil Eriksson
Date: Thu, 16 Oct 2025 20:07:06 +0200
Subject: [PATCH 5/6] fix lint error
---
packages/loaders/loaders/src/index.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/loaders/loaders/src/index.ts b/packages/loaders/loaders/src/index.ts
index 0b62561fc8..e0b8b1ade4 100644
--- a/packages/loaders/loaders/src/index.ts
+++ b/packages/loaders/loaders/src/index.ts
@@ -177,14 +177,14 @@ export class LoadersRegistry {
const typeMap = schema.getTypeMap();
// Find all types with @key directive
- Object.values(typeMap).forEach(type => {
+ for (const type of Object.values(typeMap)) {
if (
(isObjectType(type) || isInterfaceType(type)) &&
type.astNode?.directives?.some(dir => dir.name.value === 'key')
) {
entityTypes.push(type.name);
}
- });
+ }
if (entityTypes.length === 0) {
return schema; // No entities found
From 7bf95f6db3f2687d439ff3569e9b4e4143ef3c72 Mon Sep 17 00:00:00 2001
From: Emil Eriksson
Date: Mon, 20 Oct 2025 08:49:07 +0200
Subject: [PATCH 6/6] Added changeset
---
.changeset/khaki-terms-call.md | 9 +++++++++
1 file changed, 9 insertions(+)
create mode 100644 .changeset/khaki-terms-call.md
diff --git a/.changeset/khaki-terms-call.md b/.changeset/khaki-terms-call.md
new file mode 100644
index 0000000000..40fae613e1
--- /dev/null
+++ b/.changeset/khaki-terms-call.md
@@ -0,0 +1,9 @@
+---
+'@graphql-inspector/commands': minor
+'@graphql-inspector/loaders': minor
+'@graphql-inspector/core': minor
+'@graphql-inspector/cli': minor
+---
+
+Apollo Federation v2 support. Introduced built-in Federation v2 directives and a new CLI flag
+--federationV2 to enable processing schemas that use them.