Skip to content

Commit

Permalink
2️⃣ 🎉 feat(new rule): require-field-of-type-query-in-mutation-result (
Browse files Browse the repository at this point in the history
  • Loading branch information
Dimitri POSTOLOV committed Sep 24, 2021
1 parent 9a46ad4 commit f2a6635
Show file tree
Hide file tree
Showing 12 changed files with 261 additions and 18 deletions.
5 changes: 5 additions & 0 deletions .changeset/chilly-seals-explain.md
@@ -0,0 +1,5 @@
---
'@graphql-eslint/eslint-plugin': minor
---

add new rule `require-field-of-type-query-in-mutation-result`
3 changes: 2 additions & 1 deletion docs/README.md
Expand Up @@ -44,9 +44,10 @@ Name            &nbs
[possible-fragment-spread](rules/possible-fragment-spread.md)|A fragment spread is only valid if the type condition could ever possibly be true: if there is a non-empty intersection of the possible parent types, and possible types which pass the type condition.|🔮||✅
[possible-type-extension](rules/possible-type-extension.md)|A type extension is only valid if the type is defined and has the same kind.|🔮||✅
[provided-required-arguments](rules/provided-required-arguments.md)|A field or directive is only valid if all required (non-null without a default value) field arguments have been provided.|🔮||✅
[require-deprecation-date](rules/require-deprecation-date.md)|Require deletion date on @deprecated directive. Suggest to remove deprecated things after deprecated date.|🚀||
[require-deprecation-date](rules/require-deprecation-date.md)|Require deletion date on `@deprecated` directive. Suggest removing deprecated things after deprecated date.|🚀||
[require-deprecation-reason](rules/require-deprecation-reason.md)|Require all deprecation directives to specify a reason.|🚀||✅
[require-description](rules/require-description.md)|Enforce descriptions in your type definitions.|🚀||
[require-field-of-type-query-in-mutation-result](rules/require-field-of-type-query-in-mutation-result.md)|Allow the client in one round-trip not only to call mutation but also to get a wagon of data to update their application.|🚀||
[require-id-when-available](rules/require-id-when-available.md)|Enforce selecting specific fields when they are available on the GraphQL type.|🚀||
[scalar-leafs](rules/scalar-leafs.md)|A GraphQL document is valid only if all leaf fields (fields without sub selections) are of scalar or enum types.|🔮||✅
[selection-set-depth](rules/selection-set-depth.md)|Limit the complexity of the GraphQL operations solely by their depth. Based on [graphql-depth-limit](https://github.com/stems/graphql-depth-limit).|🚀||
Expand Down
2 changes: 1 addition & 1 deletion docs/rules/require-deprecation-date.md
Expand Up @@ -5,7 +5,7 @@
- Requires GraphQL Schema: `false` [ℹ️](../../README.md#extended-linting-rules-with-graphql-schema)
- Requires GraphQL Operations: `false` [ℹ️](../../README.md#extended-linting-rules-with-siblings-operations)

Require deletion date on @deprecated directive. Suggest to remove deprecated things after deprecated date.
Require deletion date on `@deprecated` directive. Suggest removing deprecated things after deprecated date.

## Usage Examples

Expand Down
42 changes: 42 additions & 0 deletions docs/rules/require-field-of-type-query-in-mutation-result.md
@@ -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!
}
```
1 change: 1 addition & 0 deletions packages/plugin/src/configs/all.ts
Expand Up @@ -20,6 +20,7 @@ export const allConfig = {
'@graphql-eslint/no-unused-fields': 'error',
'@graphql-eslint/require-deprecation-date': 'error',
'@graphql-eslint/require-description': 'error',
'@graphql-eslint/require-field-of-type-query-in-mutation-result': 'error',
'@graphql-eslint/require-id-when-available': 'error',
'@graphql-eslint/selection-set-depth': 'error',
'@graphql-eslint/unique-fragment-name': 'error',
Expand Down
Expand Up @@ -47,7 +47,7 @@ const rule: GraphQLESLintRule = {
if (isScalarType(graphQLType)) {
context.report({
node,
message: `Unexpected scalar result type "${typeName}"`,
message: `Unexpected scalar result type "${typeName}".`,
});
}
},
Expand Down
2 changes: 2 additions & 0 deletions packages/plugin/src/rules/index.ts
Expand Up @@ -21,6 +21,7 @@ import noUnusedFields from './no-unused-fields';
import requireDeprecationDate from './require-deprecation-date';
import requireDeprecationReason from './require-deprecation-reason';
import requireDescription from './require-description';
import requireFieldOfTypeQueryInMutationResult from './require-field-of-type-query-in-mutation-result';
import requireIdWhenAvailable from './require-id-when-available';
import selectionSetDepth from './selection-set-depth';
import strictIdInTypes from './strict-id-in-types';
Expand All @@ -47,6 +48,7 @@ export const rules = {
'require-deprecation-date': requireDeprecationDate,
'require-deprecation-reason': requireDeprecationReason,
'require-description': requireDescription,
'require-field-of-type-query-in-mutation-result': requireFieldOfTypeQueryInMutationResult,
'require-id-when-available': requireIdWhenAvailable,
'selection-set-depth': selectionSetDepth,
'strict-id-in-types': strictIdInTypes,
Expand Down
10 changes: 5 additions & 5 deletions packages/plugin/src/rules/require-deprecation-date.ts
Expand Up @@ -14,7 +14,7 @@ const rule: GraphQLESLintRule<[{ argumentName?: string }]> = {
docs: {
category: 'Best Practices',
description:
'Require deletion date on @deprecated directive. Suggest to remove deprecated things after deprecated date.',
'Require deletion date on `@deprecated` directive. Suggest removing deprecated things after deprecated date.',
url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/require-deprecation-date.md',
examples: [
{
Expand Down Expand Up @@ -47,10 +47,10 @@ const rule: GraphQLESLintRule<[{ argumentName?: string }]> = {
],
},
messages: {
[MESSAGE_REQUIRE_DATE]: 'Directive "@deprecated" must have a deletion date',
[MESSAGE_INVALID_FORMAT]: 'Deletion date must be in format "DD/MM/YYYY"',
[MESSAGE_INVALID_DATE]: 'Invalid "{{ deletionDate }}" deletion date',
[MESSAGE_CAN_BE_REMOVED]: '"{{ nodeName }}" сan be removed',
[MESSAGE_REQUIRE_DATE]: 'Directive "@deprecated" must have a deletion date.',
[MESSAGE_INVALID_FORMAT]: 'Deletion date must be in format "DD/MM/YYYY".',
[MESSAGE_INVALID_DATE]: 'Invalid "{{ deletionDate }}" deletion date.',
[MESSAGE_CAN_BE_REMOVED]: '"{{ nodeName }}" сan be removed.',
},
schema: [
{
Expand Down
@@ -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;
Expand Up @@ -45,7 +45,7 @@ ruleTester.runGraphQLTests('avoid-scalar-result-type-on-mutation', rule, {
createUser: Boolean
}
`),
errors: [{ message: 'Unexpected scalar result type "Boolean"' }],
errors: [{ message: 'Unexpected scalar result type "Boolean".' }],
},
{
...useSchema(/* GraphQL */ `
Expand All @@ -55,7 +55,7 @@ ruleTester.runGraphQLTests('avoid-scalar-result-type-on-mutation', rule, {
createUser: Boolean
}
`),
errors: [{ message: 'Unexpected scalar result type "Boolean"' }],
errors: [{ message: 'Unexpected scalar result type "Boolean".' }],
},
{
...useSchema(/* GraphQL */ `
Expand All @@ -67,7 +67,7 @@ ruleTester.runGraphQLTests('avoid-scalar-result-type-on-mutation', rule, {
mutation: RootMutation
}
`),
errors: [{ message: 'Unexpected scalar result type "Boolean"' }],
errors: [{ message: 'Unexpected scalar result type "Boolean".' }],
},
{
...useSchema(/* GraphQL */ `
Expand All @@ -80,7 +80,7 @@ ruleTester.runGraphQLTests('avoid-scalar-result-type-on-mutation', rule, {
mutation: RootMutation
}
`),
errors: [{ message: 'Unexpected scalar result type "Boolean"' }],
errors: [{ message: 'Unexpected scalar result type "Boolean".' }],
},
{
...useSchema(/* GraphQL */ `
Expand All @@ -91,8 +91,8 @@ ruleTester.runGraphQLTests('avoid-scalar-result-type-on-mutation', rule, {
}
`),
errors: [
{ message: 'Unexpected scalar result type "Int"' },
{ message: 'Unexpected scalar result type "Boolean"' },
{ message: 'Unexpected scalar result type "Int".' },
{ message: 'Unexpected scalar result type "Boolean".' },
],
},
],
Expand Down
8 changes: 4 additions & 4 deletions packages/plugin/tests/require-deprecation-date.spec.ts
Expand Up @@ -30,20 +30,20 @@ ruleTester.runGraphQLTests('require-deprecation-date', rule, {
invalid: [
{
code: 'scalar Old @deprecated(deletionDate: "22/08/2021")',
errors: [{ message: '"Old" сan be removed' }],
errors: [{ message: '"Old" сan be removed.' }],
},
{
code: 'scalar Old @deprecated(untilDate: "22/08/2021")',
options: [{ argumentName: 'untilDate' }],
errors: [{ message: '"Old" сan be removed' }],
errors: [{ message: '"Old" сan be removed.' }],
},
{
code: 'scalar Old @deprecated(deletionDate: "bad")',
errors: [{ message: 'Deletion date must be in format "DD/MM/YYYY"' }],
errors: [{ message: 'Deletion date must be in format "DD/MM/YYYY".' }],
},
{
code: 'scalar Old @deprecated(deletionDate: "32/08/2021")',
errors: [{ message: 'Invalid "32/08/2021" deletion date' }],
errors: [{ message: 'Invalid "32/08/2021" deletion date.' }],
},
],
});
@@ -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".' }],
},
],
});

0 comments on commit f2a6635

Please sign in to comment.