Skip to content

Commit 6cf507f

Browse files
authored
Federation Compatibility Test Suite: Successful Parts (#6071)
* Federation Compatibility Test Suite * Fixes * More tests * Fixes and more tests * Cleanup code * Do not download the tests * More * Go
1 parent 987ab0e commit 6cf507f

File tree

31 files changed

+1478
-1
lines changed

31 files changed

+1478
-1
lines changed

.changeset/great-wasps-jog.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@graphql-tools/federation": patch
3+
---
4+
5+
Handle inaccessible enum values

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ packages/load/tests/loaders/schema/test-files/error.ts
1919
packages/load/tests/loaders/schema/test-files/schema-dir/invalid.graphql
2020
packages/loaders/git/tests/test-files/type-defs-invalid.graphql
2121
packages/loaders/json-file/tests/test-files/failing/malformed.json
22+
packages/federation/test/fixtures/federation-compatibility/*
2223
.next/
2324
.bob/
2425
CHANGELOG.md

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"build:api-docs": "ts-node --transpileOnly --compiler-options='{\"module\":\"commonjs\"}' scripts/build-api-docs",
3333
"ci:lint": "cross-env \"ESLINT_USE_FLAT_CONFIG=false\" eslint --ext .ts . --output-file eslint_report.json --format json",
3434
"clean-dist": "rimraf \"packages/**/dist\" && rimraf \".bob\"",
35+
"fetch-federation-tests": "ts-node --transpileOnly --compiler-options='{\"module\":\"commonjs\"}' scripts/fetch-federation-tests",
3536
"lint": "cross-env \"ESLINT_USE_FLAT_CONFIG=false\" eslint --ext .ts .",
3637
"postinstall": "patch-package && husky install",
3738
"prerelease": "yarn build",

packages/delegate/src/Subschema.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ interface ISubschema<K = any, V = any, C = K, TContext = Record<string, any>>
2121
export class Subschema<K = any, V = any, C = K, TContext = Record<string, any>>
2222
implements ISubschema<K, V, C, TContext>
2323
{
24+
public name?: string;
2425
public schema: GraphQLSchema;
2526

2627
public executor?: Executor<TContext>;
@@ -34,6 +35,7 @@ export class Subschema<K = any, V = any, C = K, TContext = Record<string, any>>
3435
public merge?: Record<string, MergedTypeConfig<any, any, TContext>>;
3536

3637
constructor(config: SubschemaConfig<K, V, C, TContext>) {
38+
this.name = config.name;
3739
this.schema = config.schema;
3840

3941
this.executor = config.executor;

packages/delegate/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ export interface BatchingOptions<K = any, V = any, C = K> {
151151
}
152152

153153
export interface SubschemaConfig<K = any, V = any, C = K, TContext = Record<string, any>> {
154+
name?: string;
154155
schema: GraphQLSchema;
155156
createProxyingResolver?: CreateProxyingResolverFn<TContext>;
156157
rootValue?: any;

packages/federation/src/supergraph.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,8 @@ export function getSubschemasFromSupergraphSdl({
139139
const fieldDefinitionNodesOfSubgraph: FieldDefinitionNode[] = [];
140140
typeNode.fields?.forEach(fieldNode => {
141141
const joinFieldDirectives = fieldNode.directives?.filter(
142-
directiveNode => directiveNode.name.value === 'join__field',
142+
directiveNode =>
143+
directiveNode.name.value === 'join__field' && directiveNode.arguments?.length,
143144
);
144145
let notInSubgraph = true;
145146
joinFieldDirectives?.forEach(joinFieldDirectiveNode => {
@@ -691,6 +692,7 @@ export function getSubschemasFromSupergraphSdl({
691692
};
692693
}
693694
subschemaMap.set(subgraphName, {
695+
name: subgraphName,
694696
schema,
695697
executor,
696698
merge: mergeConfig,

packages/federation/src/utils.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ export function getKeyForFederation<TRoot>(root: TRoot): TRoot {
1010
}
1111

1212
export function getKeyFnForFederation(typeName: string, keys: string[]) {
13+
if (keys.some(key => key.includes('{'))) {
14+
return getKeyForFederation;
15+
}
1316
const allKeyProps = keys.flatMap(key => key.split(' '));
1417
if (allKeyProps.length > 1) {
1518
const typeNameEntry = ['__typename', typeName];
@@ -27,6 +30,11 @@ export function getKeyFnForFederation(typeName: string, keys: string[]) {
2730
}
2831

2932
export function getCacheKeyFnFromKey(key: string) {
33+
if (key.includes('{')) {
34+
return function cacheKeyFn(root: any) {
35+
return JSON.stringify(root);
36+
};
37+
}
3038
const keyProps = key.split(' ');
3139
if (keyProps.length > 1) {
3240
return function cacheKeyFn(root: any) {
@@ -60,6 +68,17 @@ export function filterInternalFieldsAndTypes(finalSchema: GraphQLSchema) {
6068
}
6169
return fieldConfig;
6270
},
71+
[MapperKind.ENUM_VALUE]: valueConfig => {
72+
if (valueConfig.astNode?.directives?.some(d => d.name.value === 'inaccessible')) {
73+
return null;
74+
}
75+
},
76+
[MapperKind.ARGUMENT]: argConfig => {
77+
if (argConfig.astNode?.directives?.some(d => d.name.value === 'inaccessible')) {
78+
return null;
79+
}
80+
return argConfig;
81+
},
6382
});
6483
}
6584

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { readdirSync, readFileSync } from 'fs';
2+
import { join } from 'path';
3+
import { parse, validate } from 'graphql';
4+
import { normalizedExecutor } from '@graphql-tools/executor';
5+
import { getStitchedSchemaFromSupergraphSdl } from '../src/supergraph';
6+
7+
describe('Federation Compatibility', () => {
8+
const fixturesDir = join(__dirname, 'fixtures', 'federation-compatibility');
9+
readdirSync(fixturesDir).forEach(supergraphName => {
10+
describe(supergraphName, () => {
11+
const supergraphFixturesDir = join(fixturesDir, supergraphName);
12+
const stitchedSchema = getStitchedSchemaFromSupergraphSdl({
13+
supergraphSdl: readFileSync(join(supergraphFixturesDir, 'supergraph.graphql'), 'utf-8'),
14+
});
15+
const tests: { query: string; expected: any }[] = JSON.parse(
16+
readFileSync(join(supergraphFixturesDir, 'tests.json'), 'utf-8'),
17+
);
18+
tests.forEach((test, i) => {
19+
it(`test-query-${i}`, async () => {
20+
let result;
21+
const document = parse(test.query, { noLocation: true });
22+
const validationErrors = validate(stitchedSchema, document);
23+
if (validationErrors.length > 0) {
24+
result = { errors: validationErrors };
25+
} else {
26+
result = await normalizedExecutor({
27+
schema: stitchedSchema,
28+
document,
29+
});
30+
}
31+
32+
if (test.expected.errors === true) {
33+
if (test.expected.data) {
34+
expect(result).toMatchObject({
35+
data: test.expected.data,
36+
errors: expect.any(Array),
37+
});
38+
} else {
39+
expect(result).toMatchObject({
40+
errors: expect.any(Array),
41+
});
42+
}
43+
} else {
44+
if ('errors' in result && result.errors) {
45+
for (const error of result.errors) {
46+
console.error(error.message);
47+
}
48+
}
49+
expect(result).toMatchObject({
50+
data: test.expected.data,
51+
});
52+
}
53+
});
54+
});
55+
});
56+
});
57+
});
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
schema
2+
@link(url: "https://specs.apollo.dev/link/v1.0")
3+
@link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
4+
{
5+
query: Query
6+
}
7+
8+
directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
9+
10+
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
11+
12+
directive @join__graph(name: String!, url: String!) on ENUM_VALUE
13+
14+
directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE
15+
16+
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
17+
18+
directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
19+
20+
directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
21+
22+
union Account
23+
@join__type(graph: B)
24+
@join__unionMember(graph: B, member: "User")
25+
@join__unionMember(graph: B, member: "Admin")
26+
= User | Admin
27+
28+
type Admin
29+
@join__type(graph: B)
30+
{
31+
id: ID
32+
photo: String
33+
}
34+
35+
scalar join__FieldSet
36+
37+
enum join__Graph {
38+
A @join__graph(name: "a", url: "https://federation-compatibility.theguild.workers.dev/entity-and-no-entity/a")
39+
B @join__graph(name: "b", url: "https://federation-compatibility.theguild.workers.dev/entity-and-no-entity/b")
40+
}
41+
42+
scalar link__Import
43+
44+
enum link__Purpose {
45+
"""
46+
`SECURITY` features provide metadata necessary to securely resolve fields.
47+
"""
48+
SECURITY
49+
50+
"""
51+
`EXECUTION` features provide metadata necessary for operation execution.
52+
"""
53+
EXECUTION
54+
}
55+
56+
type Query
57+
@join__type(graph: A)
58+
@join__type(graph: B)
59+
{
60+
users: [User!]! @join__field(graph: A)
61+
accounts: [Account!]! @join__field(graph: B)
62+
}
63+
64+
type User
65+
@join__type(graph: A)
66+
@join__type(graph: B, key: "id")
67+
{
68+
id: ID @join__field(graph: A, type: "ID") @join__field(graph: B, type: "ID!")
69+
name: String @join__field(graph: B)
70+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[
2+
{
3+
"query": "\n query {\n users {\n id\n name\n }\n accounts {\n ... on User {\n id\n name\n }\n ... on Admin {\n id\n photo\n }\n }\n }\n ",
4+
"expected": {
5+
"data": {
6+
"users": [
7+
{
8+
"id": "n1",
9+
"name": "n1-profile"
10+
}
11+
],
12+
"accounts": [
13+
{
14+
"id": "n1",
15+
"name": "n1-profile"
16+
},
17+
{
18+
"id": "a1",
19+
"photo": "a1-photo"
20+
}
21+
]
22+
}
23+
}
24+
}
25+
]

0 commit comments

Comments
 (0)