diff --git a/.changeset/@graphql-tools_import-7229-dependencies.md b/.changeset/@graphql-tools_import-7229-dependencies.md new file mode 100644 index 00000000000..3de5a346e80 --- /dev/null +++ b/.changeset/@graphql-tools_import-7229-dependencies.md @@ -0,0 +1,5 @@ +--- +"@graphql-tools/import": patch +--- +dependencies updates: + - Added dependency [`@theguild/federation-composition@^0.18.3` ↗︎](https://www.npmjs.com/package/@theguild/federation-composition/v/0.18.3) (to `dependencies`) diff --git a/.changeset/strong-melons-cry.md b/.changeset/strong-melons-cry.md new file mode 100644 index 00000000000..509f6b7dae1 --- /dev/null +++ b/.changeset/strong-melons-cry.md @@ -0,0 +1,5 @@ +--- +'@graphql-tools/import': patch +--- + +Adjust dependency and definition logic to include federated links diff --git a/packages/import/package.json b/packages/import/package.json index 909da8bd169..1ed3e89fd39 100644 --- a/packages/import/package.json +++ b/packages/import/package.json @@ -52,6 +52,7 @@ }, "dependencies": { "@graphql-tools/utils": "^10.8.6", + "@theguild/federation-composition": "^0.18.3", "resolve-from": "5.0.0", "tslib": "^2.4.0" }, diff --git a/packages/import/src/index.ts b/packages/import/src/index.ts index 32e9ad372be..15e74ac6e86 100644 --- a/packages/import/src/index.ts +++ b/packages/import/src/index.ts @@ -39,22 +39,21 @@ import { } from 'graphql'; import resolveFrom from 'resolve-from'; import { parseGraphQLSDL } from '@graphql-tools/utils'; +import { extractLinkImplementations } from '@theguild/federation-composition'; const builtinTypes = ['String', 'Float', 'Int', 'Boolean', 'ID', 'Upload']; +const federationV1Directives = ['key', 'provides', 'requires', 'external']; + const builtinDirectives = [ 'deprecated', 'skip', 'include', 'cacheControl', - 'key', - 'external', - 'requires', - 'provides', 'connection', 'client', 'specifiedBy', - 'link', + ...federationV1Directives, ]; const IMPORT_FROM_REGEX = /^import\s+(\*|(.*))\s+from\s+('|")(.*)('|");?$/; @@ -264,6 +263,66 @@ export function extractDependencies( }; } +function importFederatedSchemaLinks( + definition: DefinitionNode, + definitionsByName: Map>, +) { + const addDefinition = (name: string) => { + definitionsByName.set(name.replace(/^@/g, ''), new Set()); + }; + + // extract links from this definition + const { links, matchesImplementation, resolveImportName } = extractLinkImplementations({ + kind: Kind.DOCUMENT, + definitions: [definition], + }); + + if (links.length) { + const federationUrl = 'https://specs.apollo.dev/federation'; + const linkUrl = 'https://specs.apollo.dev/link'; + + /** + * Official Federated imports are special because they can be referenced without specifyin the import. + * To handle this case, we must prepare a list of all the possible valid usages to check against. + * Note that this versioning is not technically correct, since some definitions are after v2.0. + * But this is enough information to be comfortable not blocking the imports at this phase. It's + * the job of the composer to validate the versions. + * */ + if (matchesImplementation(federationUrl, 'v2.0')) { + const federationImports = [ + '@composeDirective', + '@extends', + '@external', + '@inaccessible', + '@interfaceObject', + '@key', + '@override', + '@provides', + '@requires', + '@shareable', + '@tag', + 'FieldSet', + ]; + for (const i of federationImports) { + addDefinition(resolveImportName(federationUrl, i)); + } + } + if (matchesImplementation(linkUrl, 'v1.0')) { + const linkImports = ['Purpose', 'Import', '@link']; + for (const i of linkImports) { + addDefinition(resolveImportName(linkUrl, i)); + } + } + + const imported = links + .filter(l => ![linkUrl, federationUrl].includes(l.identity)) + .flatMap(l => l.imports.map(i => i.as ?? i.name)); + for (const namedImport of imported) { + addDefinition(namedImport); + } + } +} + function visitDefinition( definition: DefinitionNode, definitionsByName: Map>, @@ -288,6 +347,7 @@ function visitDefinition( dependencySet = new Set(); dependenciesByDefinitionName.set(definitionName, dependencySet); } + switch (definition.kind) { case Kind.OPERATION_DEFINITION: visitOperationDefinitionNode(definition, dependencySet); @@ -317,9 +377,11 @@ function visitDefinition( visitScalarDefinitionNode(definition, dependencySet); break; case Kind.SCHEMA_DEFINITION: + importFederatedSchemaLinks(definition, definitionsByName); visitSchemaDefinitionNode(definition, dependencySet); break; case Kind.SCHEMA_EXTENSION: + importFederatedSchemaLinks(definition, definitionsByName); visitSchemaExtensionDefinitionNode(definition, dependencySet); break; case Kind.OBJECT_TYPE_EXTENSION: diff --git a/packages/import/tests/schema/fixtures/directive/h.graphql b/packages/import/tests/schema/fixtures/directive/h.graphql index a1c37e04a1a..81dcac6b934 100644 --- a/packages/import/tests/schema/fixtures/directive/h.graphql +++ b/packages/import/tests/schema/fixtures/directive/h.graphql @@ -1,8 +1,12 @@ -extend schema @link(url: "https://the-guild.dev/graphql/tools", import: ["@foo"]) +extend schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"]) + @link(url: "https://the-guild.dev/graphql/tools", import: ["@foo"]) directive @foo on FIELD_DEFINITION extend type User @key(fields: "id") { id: ID! email: String @foo + ssn: String @federation__tag(name: "private") } diff --git a/packages/import/tests/schema/import-schema.spec.ts b/packages/import/tests/schema/import-schema.spec.ts index 822fa81dec1..7ff4c781a26 100644 --- a/packages/import/tests/schema/import-schema.spec.ts +++ b/packages/import/tests/schema/import-schema.spec.ts @@ -469,13 +469,17 @@ describe('importSchema', () => { test('importSchema: link directive', () => { const expectedSDL = /* GraphQL */ ` - extend schema @link(url: "https://the-guild.dev/graphql/tools", import: ["@foo"]) + extend schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"]) + @link(url: "https://the-guild.dev/graphql/tools", import: ["@foo"]) directive @foo on FIELD_DEFINITION extend type User @key(fields: "id") { id: ID! email: String @foo + ssn: String @federation__tag(name: "private") } `; expect(importSchema('./fixtures/directive/h.graphql')).toBeSimilarGqlDoc(expectedSDL); diff --git a/yarn.lock b/yarn.lock index d4b8e11d697..8d66c2b6718 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3062,6 +3062,16 @@ tailwind-merge "^2.5.2" unist-util-visit "5.0.0" +"@theguild/federation-composition@^0.18.3": + version "0.18.3" + resolved "https://registry.yarnpkg.com/@theguild/federation-composition/-/federation-composition-0.18.3.tgz#7e475b7834f4fd0b4e5139f22b31eeaf763fdce8" + integrity sha512-UGa3R3JMDHzoKi6k32kq/bB8SbZrkkxLrfinuEPcsv0sU8r1CW+4jwCBbQGC5vnoU9asg+MuCjMYpLZSOJS/dw== + dependencies: + constant-case "^3.0.4" + debug "4.4.0" + json5 "^2.2.3" + lodash.sortby "^4.7.0" + "@theguild/prettier-config@3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@theguild/prettier-config/-/prettier-config-3.0.1.tgz#83b662197277a0de8a211c09cd43fd4cc59be614" @@ -4983,6 +4993,15 @@ consola@^3.0.0: resolved "https://registry.yarnpkg.com/consola/-/consola-3.4.2.tgz#5af110145397bb67afdab77013fdc34cae590ea7" integrity sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA== +constant-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/constant-case/-/constant-case-3.0.4.tgz#3b84a9aeaf4cf31ec45e6bf5de91bdfb0589faf1" + integrity sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + upper-case "^2.0.2" + content-disposition@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-1.0.0.tgz#844426cb398f934caefcbb172200126bc7ceace2" @@ -5464,6 +5483,13 @@ debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, d dependencies: ms "^2.1.3" +debug@4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== + dependencies: + ms "^2.1.3" + debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -12325,6 +12351,13 @@ update-browserslist-db@^1.1.3: escalade "^3.2.0" picocolors "^1.1.1" +upper-case@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-2.0.2.tgz#d89810823faab1df1549b7d97a76f8662bae6f7a" + integrity sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg== + dependencies: + tslib "^2.0.3" + uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"