Skip to content

Commit

Permalink
add new rule require-nullable-fields-with-oneof (#1330)
Browse files Browse the repository at this point in the history
  • Loading branch information
dimaMachina committed Dec 22, 2022
1 parent 2c68532 commit bab45cc
Show file tree
Hide file tree
Showing 11 changed files with 236 additions and 13 deletions.
5 changes: 5 additions & 0 deletions .changeset/early-geckos-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-eslint/eslint-plugin': minor
---

add new rule `require-nullable-fields-with-oneof`
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ Name            &nbs
[require-description](rules/require-description.md)|Enforce descriptions in type definitions and operations.|![recommended][]|📄|🚀|
[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.|![all][]|📄|🚀|
[require-id-when-available](rules/require-id-when-available.md)|Enforce selecting specific fields when they are available on the GraphQL type.|![recommended][]|📦|🚀|💡
[require-nullable-fields-with-oneof](rules/require-nullable-fields-with-oneof.md)|Require are `input` or `type` fields be non nullable with `@oneOf` directive.|![all][]|📄|🚀|
[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.|![recommended][]|📦|🔮|💡
[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://npmjs.com/package/graphql-depth-limit).|![recommended][]|📦|🚀|💡
[strict-id-in-types](rules/strict-id-in-types.md)|Requires output types to have one unique identifier unless they do not have a logical one. Exceptions can be used to ignore output types that do not have unique identifiers.|![recommended][]|📄|🚀|
Expand Down
38 changes: 38 additions & 0 deletions docs/rules/require-nullable-fields-with-oneof.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# `require-nullable-fields-with-oneof`

- Category: `Schema`
- Rule name: `@graphql-eslint/require-nullable-fields-with-oneof`
- 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 are `input` or `type` fields be non nullable with `@oneOf` directive.

## Usage Examples

### Incorrect

```graphql
# eslint @graphql-eslint/require-nullable-fields-with-oneof: 'error'

input Input @oneOf {
foo: String!
b: Int
}
```

### Correct

```graphql
# eslint @graphql-eslint/require-nullable-fields-with-oneof: 'error'

input Input @oneOf {
foo: String
bar: Int
}
```

## Resources

- [Rule source](../../packages/plugin/src/rules/require-nullable-fields-with-oneof.ts)
- [Test source](../../packages/plugin/tests/require-nullable-fields-with-oneof.spec.ts)
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"@types/node": "18.11.17",
"@typescript-eslint/eslint-plugin": "5.47.0",
"@typescript-eslint/parser": "5.47.0",
"bob-the-bundler": "4.2.0-alpha-20221222123652-a454f30",
"bob-the-bundler": "4.2.0-alpha-20221222140753-fcf5286",
"chalk": "^4.1.2",
"dedent": "0.7.0",
"enquirer": "2.3.6",
Expand Down
1 change: 1 addition & 0 deletions packages/plugin/src/configs/schema-all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ export default {
'@graphql-eslint/no-scalar-result-type-on-mutation': 'error',
'@graphql-eslint/require-deprecation-date': 'error',
'@graphql-eslint/require-field-of-type-query-in-mutation-result': 'error',
'@graphql-eslint/require-nullable-fields-with-oneof': 'error',
},
};
2 changes: 2 additions & 0 deletions packages/plugin/src/rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { rule as requireDeprecationReason } from './require-deprecation-reason';
import { rule as requireDescription } from './require-description';
import { rule as requireFieldOfTypeQueryInMutationResult } from './require-field-of-type-query-in-mutation-result';
import { rule as requireIdWhenAvailable } from './require-id-when-available';
import { rule as requireNullableFieldsWithOneof } from './require-nullable-fields-with-oneof';
import { rule as selectionSetDepth } from './selection-set-depth';
import { rule as strictIdInTypes } from './strict-id-in-types';
import { rule as uniqueFragmentName } from './unique-fragment-name';
Expand Down Expand Up @@ -60,6 +61,7 @@ export const rules = {
'require-description': requireDescription,
'require-field-of-type-query-in-mutation-result': requireFieldOfTypeQueryInMutationResult,
'require-id-when-available': requireIdWhenAvailable,
'require-nullable-fields-with-oneof': requireNullableFieldsWithOneof,
'selection-set-depth': selectionSetDepth,
'strict-id-in-types': strictIdInTypes,
'unique-fragment-name': uniqueFragmentName,
Expand Down
64 changes: 64 additions & 0 deletions packages/plugin/src/rules/require-nullable-fields-with-oneof.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { GraphQLESLintRule } from '../types';
import { InputObjectTypeDefinitionNode, Kind, ObjectTypeDefinitionNode } from 'graphql';
import { GraphQLESTreeNode } from '../estree-converter';

const RULE_ID = 'require-nullable-fields-with-oneof';

export const rule: GraphQLESLintRule = {
meta: {
type: 'suggestion',
docs: {
category: 'Schema',
description: 'Require are `input` or `type` fields be non nullable with `@oneOf` directive.',
url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
examples: [
{
title: 'Incorrect',
code: /* GraphQL */ `
input Input @oneOf {
foo: String!
b: Int
}
`,
},
{
title: 'Correct',
code: /* GraphQL */ `
input Input @oneOf {
foo: String
bar: Int
}
`,
},
],
},
messages: {
[RULE_ID]: 'Field `{{fieldName}}` must be nullable.',
},
schema: [],
},
create(context) {
return {
'Directive[name.value=oneOf]'(node: {
parent: GraphQLESTreeNode<InputObjectTypeDefinitionNode | ObjectTypeDefinitionNode>;
}) {
const isTypeOrInput = [
Kind.OBJECT_TYPE_DEFINITION,
Kind.INPUT_OBJECT_TYPE_DEFINITION,
].includes(node.parent.kind);
if (!isTypeOrInput) {
return;
}
for (const field of node.parent.fields) {
if (field.gqlType.kind === Kind.NON_NULL_TYPE) {
context.report({
node: field.name,
messageId: RULE_ID,
data: { fieldName: field.name.value },
});
}
}
},
};
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Vitest Snapshot v1

exports[`should validate \`input\` 1`] = `
#### ⌨️ Code

1 | input Input @oneOf {
2 | foo: String!
3 | bar: [Int]!
4 | }

#### ❌ Error 1/2

1 | input Input @oneOf {
> 2 | foo: String!
| ^^^ Field \`foo\` must be nullable.
3 | bar: [Int]!

#### ❌ Error 2/2

2 | foo: String!
> 3 | bar: [Int]!
| ^^^ Field \`bar\` must be nullable.
4 | }
`;

exports[`should validate \`type\` 1`] = `
#### ⌨️ Code

1 | type Type @oneOf {
2 | foo: String!
3 | bar: Int
4 | }

#### ❌ Error

1 | type Type @oneOf {
> 2 | foo: String!
| ^^^ Field \`foo\` must be nullable.
3 | bar: Int
`;
46 changes: 46 additions & 0 deletions packages/plugin/tests/require-nullable-fields-with-oneof.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { GraphQLRuleTester } from '../src';
import { rule } from '../src/rules/require-nullable-fields-with-oneof';

const ruleTester = new GraphQLRuleTester();

ruleTester.runGraphQLTests('require-nullable-fields-with-oneof', rule, {
valid: [
/* GraphQL */ `
input Input @oneOf {
foo: [String]
bar: Int
}
`,
/* GraphQL */ `
type User @oneOf {
foo: String
bar: [Int!]
}
`,
],
invalid: [
{
name: 'should validate `input`',
code: /* GraphQL */ `
input Input @oneOf {
foo: String!
bar: [Int]!
}
`,
errors: [
{ message: 'Field `foo` must be nullable.' },
{ message: 'Field `bar` must be nullable.' },
],
},
{
name: 'should validate `type`',
code: /* GraphQL */ `
type Type @oneOf {
foo: String!
bar: Int
}
`,
errors: [{ message: 'Field `foo` must be nullable.' }],
},
],
});
42 changes: 35 additions & 7 deletions pnpm-lock.yaml

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

8 changes: 3 additions & 5 deletions scripts/create-rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ import { GraphQLESLintRule } from '../types';
const RULE_ID = '${ruleId}';
const rule: GraphQLESLintRule = {
export const rule: GraphQLESLintRule = {
meta: {
type: '${type}',
docs: {
Expand Down Expand Up @@ -95,20 +95,18 @@ const rule: GraphQLESLintRule = {
};
},
};
export default rule;
`;

await writeFile(RULE_PATH, RULE_CONTENT.trimStart());

const TEST_PATH = join(CWD, `packages/plugin/tests/${ruleId}.spec.ts`);
const TEST_CONTENT = `
import { GraphQLRuleTester, ParserOptions } from '../src';
import rule from '../src/rules/${ruleId}';
import { rule } from '../src/rules/${ruleId}';
const ruleTester = new GraphQLRuleTester();
function useSchema(code: string): { code: string; parserOptions: ParserOptions } {
function useSchema(code: string): { code: string; parserOptions: Omit<ParserOptions, 'filePath'> } {
return {
code,
parserOptions: {
Expand Down

0 comments on commit bab45cc

Please sign in to comment.