Skip to content

Commit

Permalink
lint federation subgraphs schemas without parse errors (#2814)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
dimaMachina and github-actions[bot] authored Dec 5, 2024
1 parent e8ba435 commit ccc302d
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 27 deletions.
6 changes: 6 additions & 0 deletions .changeset/@graphql-eslint_eslint-plugin-2814-dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@graphql-eslint/eslint-plugin": patch
---
dependencies updates:
- 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`)
- Added dependency [`@apollo/subgraph@^2` ↗︎](https://www.npmjs.com/package/@apollo/subgraph/v/2.0.0) (to `peerDependencies`)
5 changes: 5 additions & 0 deletions .changeset/fluffy-bottles-switch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-eslint/eslint-plugin': minor
---

lint federation subgraphs schemas without parse errors
28 changes: 28 additions & 0 deletions packages/plugin/__tests__/federation.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { GRAPHQL_JS_VALIDATIONS } from '../src/rules/graphql-js-validation.js';
import { ruleTester, withSchema } from './test-utils.js';

ruleTester.run('federation', GRAPHQL_JS_VALIDATIONS['known-directives'], {
valid: [
withSchema({
name: 'should parse federation directive without errors',
code: /* GraphQL */ `
scalar DateTime
type Post @key(fields: "id") {
id: ID!
title: String
createdAt: DateTime
modifiedAt: DateTime
}
type Query {
post: Post!
posts: [Post!]
}
extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"])
`,
}),
],
invalid: [],
});
7 changes: 6 additions & 1 deletion packages/plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,22 @@
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"@apollo/subgraph": "^2",
"eslint": ">=8.44.0",
"graphql": "^16",
"json-schema-to-ts": "^3"
},
"peerDependenciesMeta": {
"@apollo/subgraph": {
"optional": true
},
"json-schema-to-ts": {
"optional": true
}
},
"dependencies": {
"@graphql-tools/code-file-loader": "^8.0.0",
"@graphql-tools/graphql-tag-pluck": "8.3.4",
"@graphql-tools/graphql-tag-pluck": "^8.3.4",
"@graphql-tools/utils": "^10.0.0",
"debug": "^4.3.4",
"fast-glob": "^3.2.12",
Expand All @@ -55,6 +59,7 @@
"lodash.lowercase": "^4.3.0"
},
"devDependencies": {
"@apollo/subgraph": "^2.9.3",
"@theguild/eslint-rule-tester": "workspace:*",
"@types/debug": "4.1.12",
"@types/eslint": "9.6.1",
Expand Down
34 changes: 32 additions & 2 deletions packages/plugin/src/schema.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import debugFactory from 'debug';
import fg from 'fast-glob';
import { GraphQLSchema } from 'graphql';
import { BREAK, GraphQLSchema, visit } from 'graphql';
import { GraphQLProjectConfig } from 'graphql-config';
import { ModuleCache } from './cache.js';
import { Pointer, Schema } from './types.js';
Expand All @@ -22,9 +22,39 @@ export function getSchema(project: GraphQLProjectConfig): Schema {
}

debug('Loading schema from %o', project.schema);
const schema = project.loadSchemaSync(project.schema, 'GraphQLSchema', {

const opts = {
pluckConfig: project.extensions.pluckConfig,
};

const typeDefs = project.loadSchemaSync(project.schema, 'DocumentNode', opts);
let isFederation = false;

visit(typeDefs, {
SchemaExtension(node) {
const linkDirective = node.directives?.find(d => d.name.value === 'link');
if (!linkDirective) return BREAK;

const urlArgument = linkDirective.arguments?.find(a => a.name.value === 'url');
if (!urlArgument) return BREAK;

if (urlArgument.value.kind !== 'StringValue') return BREAK;

isFederation = urlArgument.value.value.includes('specs.apollo.dev/federation/');
},
});

let schema: GraphQLSchema;

if (isFederation) {
// eslint-disable-next-line @typescript-eslint/no-require-imports -- we inject createRequire in `tsup.config.ts`
const { buildSubgraphSchema } = require('@apollo/subgraph');

schema = buildSubgraphSchema({ typeDefs });
} else {
schema = project.loadSchemaSync(project.schema, 'GraphQLSchema', opts);
}

if (debug.enabled) {
debug('Schema loaded: %o', schema instanceof GraphQLSchema);
const schemaPaths = fg.sync(project.schema as Pointer, { absolute: true });
Expand Down
25 changes: 25 additions & 0 deletions packages/plugin/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,31 @@ export default defineConfig([
...opts,
outDir: 'dist/esm',
target: 'esnext',
esbuildPlugins: [
{
name: 'inject-create-require',
setup(build) {
build.onLoad({ filter: /schema\.ts$/ }, async args => {
const code = await fs.readFile(args.path, 'utf8');
const index = code.indexOf('export function getSchema');

if (index === -1) {
throw new Error('Unable to inject `createRequire` for file ' + args.path);
}

return {
contents: [
'import { createRequire } from "module"',
code.slice(0, index),
'const require = createRequire(import.meta.url)',
code.slice(index),
].join('\n'),
loader: 'ts',
};
});
},
},
],
},
{
...opts,
Expand Down
78 changes: 54 additions & 24 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit ccc302d

Please sign in to comment.