Skip to content
6 changes: 6 additions & 0 deletions .changeset/lovely-emus-shake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@graphql-eslint/eslint-plugin': major
---

Add new config option `ignoredFieldSelectors` to `no-unused-fields` rule to ignore all the relay
pagination fields for every connection exposed in schema for example
14 changes: 14 additions & 0 deletions packages/plugin/src/rules/no-unused-fields/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ const ruleTester = new RuleTester<ParserOptionsForTests>({
},
});

const example = rule.meta.docs!.examples!.find(example => example.title.includes('ignoring'));
const [RELAY_SCHEMA, RELAY_QUERY] = example!.code.split('### 2️⃣ YOUR QUERY');

ruleTester.run('no-unused-fields', rule, {
valid: [
{
Expand Down Expand Up @@ -95,6 +98,17 @@ ruleTester.run('no-unused-fields', rule, {
},
},
},
{
name: 'should do not report unused fields for Relay',
options: example!.usage,
code: RELAY_SCHEMA,
parserOptions: {
graphQLConfig: {
documents: RELAY_QUERY,
schema: RELAY_SCHEMA,
},
},
},
],
invalid: [
{
Expand Down
125 changes: 118 additions & 7 deletions packages/plugin/src/rules/no-unused-fields/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,109 @@
import { GraphQLSchema, TypeInfo, visit, visitWithTypeInfo } from 'graphql';
import { FieldDefinitionNode, GraphQLSchema, TypeInfo, visit, visitWithTypeInfo } from 'graphql';
import { GraphQLProjectConfig } from 'graphql-config';
import { FromSchema } from 'json-schema-to-ts';
import { ModuleCache } from '../../cache.js';
import { SiblingOperations } from '../../siblings.js';
import { GraphQLESLintRule } from '../../types.js';
import { GraphQLESLintRule, GraphQLESTreeNode } from '../../types.js';
import { requireGraphQLSchemaFromContext, requireSiblingsOperations } from '../../utils.js';

const RULE_ID = 'no-unused-fields';

const RELAY_SCHEMA = /* GraphQL */ `
# Root Query Type
type Query {
user: User
}

# User Type
type User {
id: ID!
name: String!
friends(first: Int, after: String): FriendConnection!
}

# FriendConnection Type (Relay Connection)
type FriendConnection {
edges: [FriendEdge]
pageInfo: PageInfo!
}

# FriendEdge Type
type FriendEdge {
cursor: String!
node: Friend!
}

# Friend Type
type Friend {
id: ID!
name: String!
}

# PageInfo Type (Relay Pagination)
type PageInfo {
hasPreviousPage: Boolean!
hasNextPage: Boolean!
startCursor: String
endCursor: String
}
`;

const RELAY_QUERY = /* GraphQL */ `
query {
user {
id
name
friends(first: 10) {
edges {
node {
id
name
}
}
}
}
}
`;

const RELAY_DEFAULT_IGNORED_FIELD_SELECTORS = [
'[parent.name.value=PageInfo][name.value=/(endCursor|startCursor|hasNextPage|hasPreviousPage)/]',
'[parent.name.value=/Edge$/][name.value=cursor]',
'[parent.name.value=/Connection$/][name.value=pageInfo]',
];

const schema = {
type: 'array',
maxItems: 1,
items: {
type: 'object',
additionalProperties: false,
properties: {
ignoredFieldSelectors: {
type: 'array',
uniqueItems: true,
minItems: 1,
description: [
'Fields that will be ignored and are allowed to be unused.',
'',
'E.g. The following selector will ignore all the relay pagination fields for every connection exposed in the schema:',
'```json',
JSON.stringify(RELAY_DEFAULT_IGNORED_FIELD_SELECTORS, null, 2),
'```',
'',
'> These fields are defined by ESLint [`selectors`](https://eslint.org/docs/developer-guide/selectors).',
'> Paste or drop code into the editor in [ASTExplorer](https://astexplorer.net) and inspect the generated AST to compose your selector.',
].join('\n'),
items: {
type: 'string',
pattern: '^\\[(.+)]$',
},
},
},
},
} as const;

export type RuleOptions = FromSchema<typeof schema>;

type UsedFields = Record<string, Set<string>>;

const usedFieldsCache = new ModuleCache<GraphQLProjectConfig['schema'], UsedFields>();
Expand Down Expand Up @@ -44,7 +141,7 @@ function getUsedFields(schema: GraphQLSchema, operations: SiblingOperations): Us
return usedFields;
}

export const rule: GraphQLESLintRule = {
export const rule: GraphQLESLintRule<RuleOptions> = {
meta: {
messages: {
[RULE_ID]: 'Field "{{fieldName}}" is unused',
Expand Down Expand Up @@ -99,23 +196,37 @@ export const rule: GraphQLESLintRule = {
}
`,
},
{
title: 'Correct (ignoring fields)',
usage: [{ ignoredFieldSelectors: RELAY_DEFAULT_IGNORED_FIELD_SELECTORS }],
code: /* GraphQL */ `
### 1️⃣ YOUR SCHEMA
${RELAY_SCHEMA}

### 2️⃣ YOUR QUERY
${RELAY_QUERY}
`,
},
],
},
type: 'suggestion',
schema: [],
schema,
hasSuggestions: true,
},
create(context) {
const schema = requireGraphQLSchemaFromContext(RULE_ID, context);
const siblingsOperations = requireSiblingsOperations(RULE_ID, context);
const usedFields = getUsedFields(schema, siblingsOperations);

const { ignoredFieldSelectors } = context.options[0] || {};
const selector = (ignoredFieldSelectors || []).reduce(
(acc, selector) => `${acc}:not(${selector})`,
'FieldDefinition',
);
return {
FieldDefinition(node) {
[selector](node: GraphQLESTreeNode<FieldDefinitionNode>) {
const fieldName = node.name.value;
const parentTypeName = node.parent.name.value;
const isUsed = usedFields[parentTypeName]?.has(fieldName);

if (isUsed) {
return;
}
Expand Down
9 changes: 6 additions & 3 deletions scripts/generate-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,12 @@ async function generateDocs(): Promise<void> {
const prettierConfigTs = await prettier.resolveConfig('./_meta.ts');

const result = Object.entries(rules).map(async ([ruleName, rule]) => {
const frontMatterDescription = rule.meta
.docs!.description!.replace(/\n.*/g, '')
.replace(MARKDOWN_LINK_RE, '$1');
const blocks: string[] = [
'---',
`description: ${JSON.stringify(rule.meta.docs!.description!.replace(/\n.*/g, '').replace(MARKDOWN_LINK_RE, '$1'))}`,
`description: ${JSON.stringify(frontMatterDescription)}`,
'---',
`# \`${ruleName}\``,
];
Expand Down Expand Up @@ -98,7 +101,7 @@ async function generateDocs(): Promise<void> {
`- Requires GraphQL Schema: \`${requiresSchema}\` [ℹ️](/docs/getting-started#extended-linting-rules-with-graphql-schema)`,
`- Requires GraphQL Operations: \`${requiresSiblings}\` [ℹ️](/docs/getting-started#extended-linting-rules-with-siblings-operations)`,
BR,
docs.description,
docs.description === frontMatterDescription ? '{frontMatter.description}' : docs.description,
);

if (docs.examples?.length > 0) {
Expand Down Expand Up @@ -153,7 +156,7 @@ async function generateDocs(): Promise<void> {
);
}
return {
path: resolve(RULES_PATH, `${ruleName}.md`),
path: resolve(RULES_PATH, `${ruleName}.mdx`),
content: blocks.join('\n'),
};
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ fix some of the problems reported by this rule.
- Requires GraphQL Operations: `false`
[ℹ️](/docs/getting-started#extended-linting-rules-with-siblings-operations)

Enforce arrange in alphabetical order for type fields, enum values, input object fields, operation
selections and more.
{frontMatter.description}

## Usage Examples

Expand Down Expand Up @@ -165,12 +164,12 @@ Definitions – `type`, `interface`, `enum`, `scalar`, `input`, `union` and `dir

### `groups` (array)

Custom order group. Example: `['...', 'id', '*', '{']` where:
Order group. Example: `['...', 'id', '*', '{']` where:

- `...` stands for fragment spreads
- `id` stands for field with name `id`
- `*` stands for everything else
- `{` stands for field `selection set`
- `{` stands for fields `selection set`

The object is an array with all elements of the type `string`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ enables this rule.
- Requires GraphQL Operations: `false`
[ℹ️](/docs/getting-started#extended-linting-rules-with-siblings-operations)

Require all comments to follow the same style (either block or inline).
{frontMatter.description}

## Usage Examples

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ description:
- Requires GraphQL Operations: `false`
[ℹ️](/docs/getting-started#extended-linting-rules-with-siblings-operations)

Require queries, mutations, subscriptions or fragments to be located in separate files.
{frontMatter.description}

## Usage Examples

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ description: 'This rule allows you to enforce that the file name should match th
- Requires GraphQL Operations: `false`
[ℹ️](/docs/getting-started#extended-linting-rules-with-siblings-operations)

This rule allows you to enforce that the file name should match the operation name.
{frontMatter.description}

## Usage Examples

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ rule.
- Requires GraphQL Operations: `false`
[ℹ️](/docs/getting-started#extended-linting-rules-with-siblings-operations)

Require names to follow specified conventions.
{frontMatter.description}

## Usage Examples

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ enables this rule.
- Requires GraphQL Operations: `false`
[ℹ️](/docs/getting-started#extended-linting-rules-with-siblings-operations)

Require name for your GraphQL operations. This is useful since most GraphQL client libraries are
using the operation name for caching purposes.
{frontMatter.description}

## Usage Examples

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ enables this rule.
- Requires GraphQL Operations: `false`
[ℹ️](/docs/getting-started#extended-linting-rules-with-siblings-operations)

Enforce that deprecated fields or enum values are not in use by operations.
{frontMatter.description}

## Usage Examples

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ enables this rule.
- Requires GraphQL Operations: `false`
[ℹ️](/docs/getting-started#extended-linting-rules-with-siblings-operations)

Checks for duplicate fields in selection set, variables in operation definition, or in arguments set
of a field.
{frontMatter.description}

## Usage Examples

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ description: 'Disallow fragments that are used only in one place.'
- Requires GraphQL Operations: `true`
[ℹ️](/docs/getting-started#extended-linting-rules-with-siblings-operations)

Disallow fragments that are used only in one place.
{frontMatter.description}

## Usage Examples

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ description: 'Disallow using root types `mutation` and/or `subscription`.'
- Requires GraphQL Operations: `false`
[ℹ️](/docs/getting-started#extended-linting-rules-with-siblings-operations)

Disallow using root types `mutation` and/or `subscription`.
{frontMatter.description}

## Usage Examples

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ description: 'Avoid scalar result type on mutation type to make sure to return a
- Requires GraphQL Operations: `false`
[ℹ️](/docs/getting-started#extended-linting-rules-with-siblings-operations)

Avoid scalar result type on mutation type to make sure to return a valid state.
{frontMatter.description}

## Usage Examples

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ enables this rule.
- Requires GraphQL Operations: `false`
[ℹ️](/docs/getting-started#extended-linting-rules-with-siblings-operations)

Enforces users to avoid using the type name in a field name while defining your schema.
{frontMatter.description}

## Usage Examples

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ enables this rule.
- Requires GraphQL Operations: `false`
[ℹ️](/docs/getting-started#extended-linting-rules-with-siblings-operations)

Requires all types to be reachable at some level by root level fields.
{frontMatter.description}

## Usage Examples

Expand Down
Loading