Skip to content

Commit ccc302d

Browse files
lint federation subgraphs schemas without parse errors (#2814)
* a * chore(dependencies): updated changesets for modified dependencies * pnpm i * lint * Update packages/plugin/src/schema.ts --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent e8ba435 commit ccc302d

File tree

7 files changed

+156
-27
lines changed

7 files changed

+156
-27
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@graphql-eslint/eslint-plugin": patch
3+
---
4+
dependencies updates:
5+
- Updated dependency [`@graphql-tools/graphql-tag-pluck@^8.3.4` ↗︎](https://www.npmjs.com/package/@graphql-tools/graphql-tag-pluck/v/8.3.4) (from `8.3.4`, in `dependencies`)
6+
- Added dependency [`@apollo/subgraph@^2` ↗︎](https://www.npmjs.com/package/@apollo/subgraph/v/2.0.0) (to `peerDependencies`)

.changeset/fluffy-bottles-switch.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphql-eslint/eslint-plugin': minor
3+
---
4+
5+
lint federation subgraphs schemas without parse errors
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { GRAPHQL_JS_VALIDATIONS } from '../src/rules/graphql-js-validation.js';
2+
import { ruleTester, withSchema } from './test-utils.js';
3+
4+
ruleTester.run('federation', GRAPHQL_JS_VALIDATIONS['known-directives'], {
5+
valid: [
6+
withSchema({
7+
name: 'should parse federation directive without errors',
8+
code: /* GraphQL */ `
9+
scalar DateTime
10+
11+
type Post @key(fields: "id") {
12+
id: ID!
13+
title: String
14+
createdAt: DateTime
15+
modifiedAt: DateTime
16+
}
17+
18+
type Query {
19+
post: Post!
20+
posts: [Post!]
21+
}
22+
23+
extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"])
24+
`,
25+
}),
26+
],
27+
invalid: [],
28+
});

packages/plugin/package.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -35,18 +35,22 @@
3535
"typecheck": "tsc --noEmit"
3636
},
3737
"peerDependencies": {
38+
"@apollo/subgraph": "^2",
3839
"eslint": ">=8.44.0",
3940
"graphql": "^16",
4041
"json-schema-to-ts": "^3"
4142
},
4243
"peerDependenciesMeta": {
44+
"@apollo/subgraph": {
45+
"optional": true
46+
},
4347
"json-schema-to-ts": {
4448
"optional": true
4549
}
4650
},
4751
"dependencies": {
4852
"@graphql-tools/code-file-loader": "^8.0.0",
49-
"@graphql-tools/graphql-tag-pluck": "8.3.4",
53+
"@graphql-tools/graphql-tag-pluck": "^8.3.4",
5054
"@graphql-tools/utils": "^10.0.0",
5155
"debug": "^4.3.4",
5256
"fast-glob": "^3.2.12",
@@ -55,6 +59,7 @@
5559
"lodash.lowercase": "^4.3.0"
5660
},
5761
"devDependencies": {
62+
"@apollo/subgraph": "^2.9.3",
5863
"@theguild/eslint-rule-tester": "workspace:*",
5964
"@types/debug": "4.1.12",
6065
"@types/eslint": "9.6.1",

packages/plugin/src/schema.ts

+32-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import debugFactory from 'debug';
22
import fg from 'fast-glob';
3-
import { GraphQLSchema } from 'graphql';
3+
import { BREAK, GraphQLSchema, visit } from 'graphql';
44
import { GraphQLProjectConfig } from 'graphql-config';
55
import { ModuleCache } from './cache.js';
66
import { Pointer, Schema } from './types.js';
@@ -22,9 +22,39 @@ export function getSchema(project: GraphQLProjectConfig): Schema {
2222
}
2323

2424
debug('Loading schema from %o', project.schema);
25-
const schema = project.loadSchemaSync(project.schema, 'GraphQLSchema', {
25+
26+
const opts = {
2627
pluckConfig: project.extensions.pluckConfig,
28+
};
29+
30+
const typeDefs = project.loadSchemaSync(project.schema, 'DocumentNode', opts);
31+
let isFederation = false;
32+
33+
visit(typeDefs, {
34+
SchemaExtension(node) {
35+
const linkDirective = node.directives?.find(d => d.name.value === 'link');
36+
if (!linkDirective) return BREAK;
37+
38+
const urlArgument = linkDirective.arguments?.find(a => a.name.value === 'url');
39+
if (!urlArgument) return BREAK;
40+
41+
if (urlArgument.value.kind !== 'StringValue') return BREAK;
42+
43+
isFederation = urlArgument.value.value.includes('specs.apollo.dev/federation/');
44+
},
2745
});
46+
47+
let schema: GraphQLSchema;
48+
49+
if (isFederation) {
50+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- we inject createRequire in `tsup.config.ts`
51+
const { buildSubgraphSchema } = require('@apollo/subgraph');
52+
53+
schema = buildSubgraphSchema({ typeDefs });
54+
} else {
55+
schema = project.loadSchemaSync(project.schema, 'GraphQLSchema', opts);
56+
}
57+
2858
if (debug.enabled) {
2959
debug('Schema loaded: %o', schema instanceof GraphQLSchema);
3060
const schemaPaths = fg.sync(project.schema as Pointer, { absolute: true });

packages/plugin/tsup.config.ts

+25
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,31 @@ export default defineConfig([
2727
...opts,
2828
outDir: 'dist/esm',
2929
target: 'esnext',
30+
esbuildPlugins: [
31+
{
32+
name: 'inject-create-require',
33+
setup(build) {
34+
build.onLoad({ filter: /schema\.ts$/ }, async args => {
35+
const code = await fs.readFile(args.path, 'utf8');
36+
const index = code.indexOf('export function getSchema');
37+
38+
if (index === -1) {
39+
throw new Error('Unable to inject `createRequire` for file ' + args.path);
40+
}
41+
42+
return {
43+
contents: [
44+
'import { createRequire } from "module"',
45+
code.slice(0, index),
46+
'const require = createRequire(import.meta.url)',
47+
code.slice(index),
48+
].join('\n'),
49+
loader: 'ts',
50+
};
51+
});
52+
},
53+
},
54+
],
3055
},
3156
{
3257
...opts,

pnpm-lock.yaml

+54-24
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)