Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
2️⃣ 🎉 feat(new rule):
require-field-of-type-query-in-mutation-result
(
#643)
- Loading branch information
Dimitri POSTOLOV
committed
Sep 24, 2021
1 parent
9a46ad4
commit f2a6635
Showing
12 changed files
with
261 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@graphql-eslint/eslint-plugin': minor | ||
--- | ||
|
||
add new rule `require-field-of-type-query-in-mutation-result` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
42 changes: 42 additions & 0 deletions
42
docs/rules/require-field-of-type-query-in-mutation-result.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
# `require-field-of-type-query-in-mutation-result` | ||
|
||
- Category: `Best Practices` | ||
- Rule name: `@graphql-eslint/require-field-of-type-query-in-mutation-result` | ||
- Requires GraphQL Schema: `true` [ℹ️](../../README.md#extended-linting-rules-with-graphql-schema) | ||
- Requires GraphQL Operations: `false` [ℹ️](../../README.md#extended-linting-rules-with-siblings-operations) | ||
|
||
Allow the client in one round-trip not only to call mutation but also to get a wagon of data to update their application. | ||
> Currently, no errors are reported for result type `union`, `interface` and `scalar`. | ||
## Usage Examples | ||
|
||
### Incorrect | ||
|
||
```graphql | ||
# eslint @graphql-eslint/require-field-of-type-query-in-mutation-result: 'error' | ||
|
||
type User { ... } | ||
|
||
type Mutation { | ||
createUser: User! | ||
} | ||
``` | ||
|
||
### Correct | ||
|
||
```graphql | ||
# eslint @graphql-eslint/require-field-of-type-query-in-mutation-result: 'error' | ||
|
||
type User { ... } | ||
|
||
type Query { ... } | ||
|
||
type CreateUserPayload { | ||
user: User! | ||
query: Query! | ||
} | ||
|
||
type Mutation { | ||
createUser: CreateUserPayload! | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
79 changes: 79 additions & 0 deletions
79
packages/plugin/src/rules/require-field-of-type-query-in-mutation-result.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import { Kind, FieldDefinitionNode, isObjectType } from 'graphql'; | ||
import { requireGraphQLSchemaFromContext, getTypeName } from '../utils'; | ||
import { GraphQLESLintRule } from '../types'; | ||
import { GraphQLESTreeNode } from '../estree-parser'; | ||
|
||
const RULE_NAME = 'require-field-of-type-query-in-mutation-result'; | ||
|
||
const rule: GraphQLESLintRule = { | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
category: 'Best Practices', | ||
description: | ||
'Allow the client in one round-trip not only to call mutation but also to get a wagon of data to update their application.\n> Currently, no errors are reported for result type `union`, `interface` and `scalar`.', | ||
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_NAME}.md`, | ||
requiresSchema: true, | ||
examples: [ | ||
{ | ||
title: 'Incorrect', | ||
code: /* GraphQL */ ` | ||
type User { ... } | ||
type Mutation { | ||
createUser: User! | ||
} | ||
`, | ||
}, | ||
{ | ||
title: 'Correct', | ||
code: /* GraphQL */ ` | ||
type User { ... } | ||
type Query { ... } | ||
type CreateUserPayload { | ||
user: User! | ||
query: Query! | ||
} | ||
type Mutation { | ||
createUser: CreateUserPayload! | ||
} | ||
`, | ||
}, | ||
], | ||
}, | ||
}, | ||
create(context) { | ||
const schema = requireGraphQLSchemaFromContext(RULE_NAME, context); | ||
const mutationType = schema.getMutationType(); | ||
const queryType = schema.getQueryType(); | ||
|
||
if (!mutationType || !queryType) { | ||
return {}; | ||
} | ||
const selector = `:matches(${Kind.OBJECT_TYPE_DEFINITION}, ${Kind.OBJECT_TYPE_EXTENSION})[name.value=${mutationType.name}] > ${Kind.FIELD_DEFINITION}`; | ||
|
||
return { | ||
[selector](node: GraphQLESTreeNode<FieldDefinitionNode>) { | ||
const rawNode = node.rawNode(); | ||
const typeName = getTypeName(rawNode); | ||
const graphQLType = schema.getType(typeName); | ||
|
||
if (isObjectType(graphQLType)) { | ||
const { fields } = graphQLType.astNode; | ||
const hasQueryType = fields.some(field => getTypeName(field) === queryType.name); | ||
if (!hasQueryType) { | ||
context.report({ | ||
node, | ||
message: `Mutation result type "${graphQLType.name}" must contain field of type "${queryType.name}".`, | ||
}); | ||
} | ||
} | ||
}, | ||
}; | ||
}, | ||
}; | ||
|
||
export default rule; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
113 changes: 113 additions & 0 deletions
113
packages/plugin/tests/require-field-of-type-query-in-mutation-result.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import { GraphQLRuleTester, ParserOptions } from '../src'; | ||
import rule from '../src/rules/require-field-of-type-query-in-mutation-result'; | ||
|
||
const useSchema = (code: string): { code: string; parserOptions: ParserOptions } => ({ | ||
code, | ||
parserOptions: { | ||
schema: /* GraphQL */ ` | ||
type User { | ||
id: ID! | ||
} | ||
${code} | ||
`, | ||
}, | ||
}); | ||
|
||
const ruleTester = new GraphQLRuleTester(); | ||
|
||
ruleTester.runGraphQLTests('require-field-of-type-query-in-mutation-result', rule, { | ||
valid: [ | ||
useSchema(/* GraphQL */ ` | ||
type Query { | ||
user: User | ||
} | ||
`), | ||
useSchema(/* GraphQL */ ` | ||
# type Query is not defined and no error is reported | ||
type Mutation { | ||
createUser: User! | ||
} | ||
`), | ||
useSchema(/* GraphQL */ ` | ||
type Query | ||
type CreateUserPayload { | ||
user: User! | ||
query: Query! | ||
} | ||
type Mutation { | ||
createUser: CreateUserPayload! | ||
} | ||
`), | ||
useSchema(/* GraphQL */ ` | ||
# No errors are reported for type union, interface and scalar | ||
type Admin { | ||
id: ID! | ||
} | ||
union Union = User | Admin | ||
interface Interface { | ||
id: ID! | ||
} | ||
type Query | ||
type Mutation { | ||
foo: Boolean | ||
bar: Union | ||
baz: Interface | ||
} | ||
`), | ||
], | ||
invalid: [ | ||
{ | ||
...useSchema(/* GraphQL */ ` | ||
type Query | ||
type Mutation { | ||
createUser: User! | ||
} | ||
`), | ||
errors: [{ message: 'Mutation result type "User" must contain field of type "Query".' }], | ||
}, | ||
{ | ||
...useSchema(/* GraphQL */ ` | ||
type Query | ||
type Mutation | ||
extend type Mutation { | ||
createUser: User! | ||
} | ||
`), | ||
errors: [{ message: 'Mutation result type "User" must contain field of type "Query".' }], | ||
}, | ||
{ | ||
...useSchema(/* GraphQL */ ` | ||
type RootQuery | ||
type RootMutation { | ||
createUser: User! | ||
} | ||
schema { | ||
mutation: RootMutation | ||
query: RootQuery | ||
} | ||
`), | ||
errors: [{ message: 'Mutation result type "User" must contain field of type "RootQuery".' }], | ||
}, | ||
{ | ||
...useSchema(/* GraphQL */ ` | ||
type RootQuery | ||
type RootMutation | ||
extend type RootMutation { | ||
createUser: User! | ||
} | ||
schema { | ||
mutation: RootMutation | ||
query: RootQuery | ||
} | ||
`), | ||
errors: [{ message: 'Mutation result type "User" must contain field of type "RootQuery".' }], | ||
}, | ||
], | ||
}); |