Skip to content

Commit bee7f45

Browse files
jdollegithub-actions[bot]
authored andcommitted
Handle federation v2 links (#7229)
* Adjust dependency and definition logic to include federated links * chore(dependencies): updated changesets for modified dependencies --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent cb3d836 commit bee7f45

File tree

7 files changed

+121
-7
lines changed

7 files changed

+121
-7
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@graphql-tools/import": patch
3+
---
4+
dependencies updates:
5+
- Added dependency [`@theguild/federation-composition@^0.18.3` ↗︎](https://www.npmjs.com/package/@theguild/federation-composition/v/0.18.3) (to `dependencies`)

.changeset/strong-melons-cry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphql-tools/import': patch
3+
---
4+
5+
Adjust dependency and definition logic to include federated links

packages/import/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
},
5353
"dependencies": {
5454
"@graphql-tools/utils": "^10.8.6",
55+
"@theguild/federation-composition": "^0.18.3",
5556
"resolve-from": "5.0.0",
5657
"tslib": "^2.4.0"
5758
},

packages/import/src/index.ts

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,22 +39,21 @@ import {
3939
} from 'graphql';
4040
import resolveFrom from 'resolve-from';
4141
import { parseGraphQLSDL } from '@graphql-tools/utils';
42+
import { extractLinkImplementations } from '@theguild/federation-composition';
4243

4344
const builtinTypes = ['String', 'Float', 'Int', 'Boolean', 'ID', 'Upload'];
4445

46+
const federationV1Directives = ['key', 'provides', 'requires', 'external'];
47+
4548
const builtinDirectives = [
4649
'deprecated',
4750
'skip',
4851
'include',
4952
'cacheControl',
50-
'key',
51-
'external',
52-
'requires',
53-
'provides',
5453
'connection',
5554
'client',
5655
'specifiedBy',
57-
'link',
56+
...federationV1Directives,
5857
];
5958

6059
const IMPORT_FROM_REGEX = /^import\s+(\*|(.*))\s+from\s+('|")(.*)('|");?$/;
@@ -264,6 +263,66 @@ export function extractDependencies(
264263
};
265264
}
266265

266+
function importFederatedSchemaLinks(
267+
definition: DefinitionNode,
268+
definitionsByName: Map<string, Set<DefinitionNode>>,
269+
) {
270+
const addDefinition = (name: string) => {
271+
definitionsByName.set(name.replace(/^@/g, ''), new Set());
272+
};
273+
274+
// extract links from this definition
275+
const { links, matchesImplementation, resolveImportName } = extractLinkImplementations({
276+
kind: Kind.DOCUMENT,
277+
definitions: [definition],
278+
});
279+
280+
if (links.length) {
281+
const federationUrl = 'https://specs.apollo.dev/federation';
282+
const linkUrl = 'https://specs.apollo.dev/link';
283+
284+
/**
285+
* Official Federated imports are special because they can be referenced without specifyin the import.
286+
* To handle this case, we must prepare a list of all the possible valid usages to check against.
287+
* Note that this versioning is not technically correct, since some definitions are after v2.0.
288+
* But this is enough information to be comfortable not blocking the imports at this phase. It's
289+
* the job of the composer to validate the versions.
290+
* */
291+
if (matchesImplementation(federationUrl, 'v2.0')) {
292+
const federationImports = [
293+
'@composeDirective',
294+
'@extends',
295+
'@external',
296+
'@inaccessible',
297+
'@interfaceObject',
298+
'@key',
299+
'@override',
300+
'@provides',
301+
'@requires',
302+
'@shareable',
303+
'@tag',
304+
'FieldSet',
305+
];
306+
for (const i of federationImports) {
307+
addDefinition(resolveImportName(federationUrl, i));
308+
}
309+
}
310+
if (matchesImplementation(linkUrl, 'v1.0')) {
311+
const linkImports = ['Purpose', 'Import', '@link'];
312+
for (const i of linkImports) {
313+
addDefinition(resolveImportName(linkUrl, i));
314+
}
315+
}
316+
317+
const imported = links
318+
.filter(l => ![linkUrl, federationUrl].includes(l.identity))
319+
.flatMap(l => l.imports.map(i => i.as ?? i.name));
320+
for (const namedImport of imported) {
321+
addDefinition(namedImport);
322+
}
323+
}
324+
}
325+
267326
function visitDefinition(
268327
definition: DefinitionNode,
269328
definitionsByName: Map<string, Set<DefinitionNode>>,
@@ -288,6 +347,7 @@ function visitDefinition(
288347
dependencySet = new Set();
289348
dependenciesByDefinitionName.set(definitionName, dependencySet);
290349
}
350+
291351
switch (definition.kind) {
292352
case Kind.OPERATION_DEFINITION:
293353
visitOperationDefinitionNode(definition, dependencySet);
@@ -317,9 +377,11 @@ function visitDefinition(
317377
visitScalarDefinitionNode(definition, dependencySet);
318378
break;
319379
case Kind.SCHEMA_DEFINITION:
380+
importFederatedSchemaLinks(definition, definitionsByName);
320381
visitSchemaDefinitionNode(definition, dependencySet);
321382
break;
322383
case Kind.SCHEMA_EXTENSION:
384+
importFederatedSchemaLinks(definition, definitionsByName);
323385
visitSchemaExtensionDefinitionNode(definition, dependencySet);
324386
break;
325387
case Kind.OBJECT_TYPE_EXTENSION:
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
extend schema @link(url: "https://the-guild.dev/graphql/tools", import: ["@foo"])
1+
extend schema
2+
@link(url: "https://specs.apollo.dev/link/v1.0")
3+
@link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"])
4+
@link(url: "https://the-guild.dev/graphql/tools", import: ["@foo"])
25

36
directive @foo on FIELD_DEFINITION
47

58
extend type User @key(fields: "id") {
69
id: ID!
710
email: String @foo
11+
ssn: String @federation__tag(name: "private")
812
}

packages/import/tests/schema/import-schema.spec.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -469,13 +469,17 @@ describe('importSchema', () => {
469469

470470
test('importSchema: link directive', () => {
471471
const expectedSDL = /* GraphQL */ `
472-
extend schema @link(url: "https://the-guild.dev/graphql/tools", import: ["@foo"])
472+
extend schema
473+
@link(url: "https://specs.apollo.dev/link/v1.0")
474+
@link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"])
475+
@link(url: "https://the-guild.dev/graphql/tools", import: ["@foo"])
473476
474477
directive @foo on FIELD_DEFINITION
475478
476479
extend type User @key(fields: "id") {
477480
id: ID!
478481
email: String @foo
482+
ssn: String @federation__tag(name: "private")
479483
}
480484
`;
481485
expect(importSchema('./fixtures/directive/h.graphql')).toBeSimilarGqlDoc(expectedSDL);

yarn.lock

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3062,6 +3062,16 @@
30623062
tailwind-merge "^2.5.2"
30633063
unist-util-visit "5.0.0"
30643064

3065+
"@theguild/federation-composition@^0.18.3":
3066+
version "0.18.3"
3067+
resolved "https://registry.yarnpkg.com/@theguild/federation-composition/-/federation-composition-0.18.3.tgz#7e475b7834f4fd0b4e5139f22b31eeaf763fdce8"
3068+
integrity sha512-UGa3R3JMDHzoKi6k32kq/bB8SbZrkkxLrfinuEPcsv0sU8r1CW+4jwCBbQGC5vnoU9asg+MuCjMYpLZSOJS/dw==
3069+
dependencies:
3070+
constant-case "^3.0.4"
3071+
debug "4.4.0"
3072+
json5 "^2.2.3"
3073+
lodash.sortby "^4.7.0"
3074+
30653075
"@theguild/prettier-config@3.0.1":
30663076
version "3.0.1"
30673077
resolved "https://registry.yarnpkg.com/@theguild/prettier-config/-/prettier-config-3.0.1.tgz#83b662197277a0de8a211c09cd43fd4cc59be614"
@@ -4988,6 +4998,15 @@ consola@^3.0.0:
49884998
resolved "https://registry.yarnpkg.com/consola/-/consola-3.4.2.tgz#5af110145397bb67afdab77013fdc34cae590ea7"
49894999
integrity sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==
49905000

5001+
constant-case@^3.0.4:
5002+
version "3.0.4"
5003+
resolved "https://registry.yarnpkg.com/constant-case/-/constant-case-3.0.4.tgz#3b84a9aeaf4cf31ec45e6bf5de91bdfb0589faf1"
5004+
integrity sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==
5005+
dependencies:
5006+
no-case "^3.0.4"
5007+
tslib "^2.0.3"
5008+
upper-case "^2.0.2"
5009+
49915010
content-disposition@^1.0.0:
49925011
version "1.0.0"
49935012
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-1.0.0.tgz#844426cb398f934caefcbb172200126bc7ceace2"
@@ -5469,6 +5488,13 @@ debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, d
54695488
dependencies:
54705489
ms "^2.1.3"
54715490

5491+
debug@4.4.0:
5492+
version "4.4.0"
5493+
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
5494+
integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==
5495+
dependencies:
5496+
ms "^2.1.3"
5497+
54725498
debug@^3.2.7:
54735499
version "3.2.7"
54745500
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
@@ -12346,6 +12372,13 @@ update-browserslist-db@^1.1.3:
1234612372
escalade "^3.2.0"
1234712373
picocolors "^1.1.1"
1234812374

12375+
upper-case@^2.0.2:
12376+
version "2.0.2"
12377+
resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-2.0.2.tgz#d89810823faab1df1549b7d97a76f8662bae6f7a"
12378+
integrity sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==
12379+
dependencies:
12380+
tslib "^2.0.3"
12381+
1234912382
uri-js@^4.2.2:
1235012383
version "4.4.1"
1235112384
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"

0 commit comments

Comments
 (0)