Skip to content

Commit bab45cc

Browse files
authored
add new rule require-nullable-fields-with-oneof (#1330)
1 parent 2c68532 commit bab45cc

File tree

11 files changed

+236
-13
lines changed

11 files changed

+236
-13
lines changed

.changeset/early-geckos-search.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphql-eslint/eslint-plugin': minor
3+
---
4+
5+
add new rule `require-nullable-fields-with-oneof`

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ Name            &nbs
5858
[require-description](rules/require-description.md)|Enforce descriptions in type definitions and operations.|![recommended][]|📄|🚀|
5959
[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][]|📄|🚀|
6060
[require-id-when-available](rules/require-id-when-available.md)|Enforce selecting specific fields when they are available on the GraphQL type.|![recommended][]|📦|🚀|💡
61+
[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][]|📄|🚀|
6162
[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][]|📦|🔮|💡
6263
[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][]|📦|🚀|💡
6364
[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][]|📄|🚀|
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# `require-nullable-fields-with-oneof`
2+
3+
- Category: `Schema`
4+
- Rule name: `@graphql-eslint/require-nullable-fields-with-oneof`
5+
- Requires GraphQL Schema: `false` [ℹ️](../../README.md#extended-linting-rules-with-graphql-schema)
6+
- Requires GraphQL Operations: `false`
7+
[ℹ️](../../README.md#extended-linting-rules-with-siblings-operations)
8+
9+
Require are `input` or `type` fields be non nullable with `@oneOf` directive.
10+
11+
## Usage Examples
12+
13+
### Incorrect
14+
15+
```graphql
16+
# eslint @graphql-eslint/require-nullable-fields-with-oneof: 'error'
17+
18+
input Input @oneOf {
19+
foo: String!
20+
b: Int
21+
}
22+
```
23+
24+
### Correct
25+
26+
```graphql
27+
# eslint @graphql-eslint/require-nullable-fields-with-oneof: 'error'
28+
29+
input Input @oneOf {
30+
foo: String
31+
bar: Int
32+
}
33+
```
34+
35+
## Resources
36+
37+
- [Rule source](../../packages/plugin/src/rules/require-nullable-fields-with-oneof.ts)
38+
- [Test source](../../packages/plugin/tests/require-nullable-fields-with-oneof.spec.ts)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"@types/node": "18.11.17",
2929
"@typescript-eslint/eslint-plugin": "5.47.0",
3030
"@typescript-eslint/parser": "5.47.0",
31-
"bob-the-bundler": "4.2.0-alpha-20221222123652-a454f30",
31+
"bob-the-bundler": "4.2.0-alpha-20221222140753-fcf5286",
3232
"chalk": "^4.1.2",
3333
"dedent": "0.7.0",
3434
"enquirer": "2.3.6",

packages/plugin/src/configs/schema-all.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@ export default {
1717
'@graphql-eslint/no-scalar-result-type-on-mutation': 'error',
1818
'@graphql-eslint/require-deprecation-date': 'error',
1919
'@graphql-eslint/require-field-of-type-query-in-mutation-result': 'error',
20+
'@graphql-eslint/require-nullable-fields-with-oneof': 'error',
2021
},
2122
};

packages/plugin/src/rules/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { rule as requireDeprecationReason } from './require-deprecation-reason';
2828
import { rule as requireDescription } from './require-description';
2929
import { rule as requireFieldOfTypeQueryInMutationResult } from './require-field-of-type-query-in-mutation-result';
3030
import { rule as requireIdWhenAvailable } from './require-id-when-available';
31+
import { rule as requireNullableFieldsWithOneof } from './require-nullable-fields-with-oneof';
3132
import { rule as selectionSetDepth } from './selection-set-depth';
3233
import { rule as strictIdInTypes } from './strict-id-in-types';
3334
import { rule as uniqueFragmentName } from './unique-fragment-name';
@@ -60,6 +61,7 @@ export const rules = {
6061
'require-description': requireDescription,
6162
'require-field-of-type-query-in-mutation-result': requireFieldOfTypeQueryInMutationResult,
6263
'require-id-when-available': requireIdWhenAvailable,
64+
'require-nullable-fields-with-oneof': requireNullableFieldsWithOneof,
6365
'selection-set-depth': selectionSetDepth,
6466
'strict-id-in-types': strictIdInTypes,
6567
'unique-fragment-name': uniqueFragmentName,
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { GraphQLESLintRule } from '../types';
2+
import { InputObjectTypeDefinitionNode, Kind, ObjectTypeDefinitionNode } from 'graphql';
3+
import { GraphQLESTreeNode } from '../estree-converter';
4+
5+
const RULE_ID = 'require-nullable-fields-with-oneof';
6+
7+
export const rule: GraphQLESLintRule = {
8+
meta: {
9+
type: 'suggestion',
10+
docs: {
11+
category: 'Schema',
12+
description: 'Require are `input` or `type` fields be non nullable with `@oneOf` directive.',
13+
url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
14+
examples: [
15+
{
16+
title: 'Incorrect',
17+
code: /* GraphQL */ `
18+
input Input @oneOf {
19+
foo: String!
20+
b: Int
21+
}
22+
`,
23+
},
24+
{
25+
title: 'Correct',
26+
code: /* GraphQL */ `
27+
input Input @oneOf {
28+
foo: String
29+
bar: Int
30+
}
31+
`,
32+
},
33+
],
34+
},
35+
messages: {
36+
[RULE_ID]: 'Field `{{fieldName}}` must be nullable.',
37+
},
38+
schema: [],
39+
},
40+
create(context) {
41+
return {
42+
'Directive[name.value=oneOf]'(node: {
43+
parent: GraphQLESTreeNode<InputObjectTypeDefinitionNode | ObjectTypeDefinitionNode>;
44+
}) {
45+
const isTypeOrInput = [
46+
Kind.OBJECT_TYPE_DEFINITION,
47+
Kind.INPUT_OBJECT_TYPE_DEFINITION,
48+
].includes(node.parent.kind);
49+
if (!isTypeOrInput) {
50+
return;
51+
}
52+
for (const field of node.parent.fields) {
53+
if (field.gqlType.kind === Kind.NON_NULL_TYPE) {
54+
context.report({
55+
node: field.name,
56+
messageId: RULE_ID,
57+
data: { fieldName: field.name.value },
58+
});
59+
}
60+
}
61+
},
62+
};
63+
},
64+
};
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Vitest Snapshot v1
2+
3+
exports[`should validate \`input\` 1`] = `
4+
#### ⌨️ Code
5+
6+
1 | input Input @oneOf {
7+
2 | foo: String!
8+
3 | bar: [Int]!
9+
4 | }
10+
11+
#### ❌ Error 1/2
12+
13+
1 | input Input @oneOf {
14+
> 2 | foo: String!
15+
| ^^^ Field \`foo\` must be nullable.
16+
3 | bar: [Int]!
17+
18+
#### ❌ Error 2/2
19+
20+
2 | foo: String!
21+
> 3 | bar: [Int]!
22+
| ^^^ Field \`bar\` must be nullable.
23+
4 | }
24+
`;
25+
26+
exports[`should validate \`type\` 1`] = `
27+
#### ⌨️ Code
28+
29+
1 | type Type @oneOf {
30+
2 | foo: String!
31+
3 | bar: Int
32+
4 | }
33+
34+
#### ❌ Error
35+
36+
1 | type Type @oneOf {
37+
> 2 | foo: String!
38+
| ^^^ Field \`foo\` must be nullable.
39+
3 | bar: Int
40+
`;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { GraphQLRuleTester } from '../src';
2+
import { rule } from '../src/rules/require-nullable-fields-with-oneof';
3+
4+
const ruleTester = new GraphQLRuleTester();
5+
6+
ruleTester.runGraphQLTests('require-nullable-fields-with-oneof', rule, {
7+
valid: [
8+
/* GraphQL */ `
9+
input Input @oneOf {
10+
foo: [String]
11+
bar: Int
12+
}
13+
`,
14+
/* GraphQL */ `
15+
type User @oneOf {
16+
foo: String
17+
bar: [Int!]
18+
}
19+
`,
20+
],
21+
invalid: [
22+
{
23+
name: 'should validate `input`',
24+
code: /* GraphQL */ `
25+
input Input @oneOf {
26+
foo: String!
27+
bar: [Int]!
28+
}
29+
`,
30+
errors: [
31+
{ message: 'Field `foo` must be nullable.' },
32+
{ message: 'Field `bar` must be nullable.' },
33+
],
34+
},
35+
{
36+
name: 'should validate `type`',
37+
code: /* GraphQL */ `
38+
type Type @oneOf {
39+
foo: String!
40+
bar: Int
41+
}
42+
`,
43+
errors: [{ message: 'Field `foo` must be nullable.' }],
44+
},
45+
],
46+
});

pnpm-lock.yaml

Lines changed: 35 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)