From 4693f27ad7fc34a8fdf94b5bf299496b131fd63a Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Mon, 8 Mar 2021 04:44:38 +1100 Subject: [PATCH] [New rule] Add strict-id-in-types rule (#333) --- .changeset/happy-trainers-doubt.md | 5 + docs/README.md | 4 +- docs/rules/executable-definitions.md | 2 +- docs/rules/fields-on-correct-type.md | 2 +- docs/rules/fragments-on-composite-type.md | 2 +- docs/rules/input-name.md | 22 +- docs/rules/known-argument-names.md | 2 +- docs/rules/known-directives.md | 2 +- docs/rules/known-fragment-names.md | 2 +- docs/rules/known-type-names.md | 2 +- docs/rules/lone-anonymous-operation.md | 2 +- docs/rules/lone-schema-definition.md | 2 +- docs/rules/no-fragment-cycles.md | 2 +- docs/rules/no-undefined-variables.md | 2 +- docs/rules/no-unreachable-types.md | 26 +- docs/rules/no-unused-fragments.md | 2 +- docs/rules/no-unused-variables.md | 2 +- docs/rules/one-field-subscriptions.md | 2 +- .../rules/overlapping-fields-can-be-merged.md | 2 +- docs/rules/possible-fragment-spread.md | 2 +- docs/rules/possible-type-extension.md | 2 +- docs/rules/provided-required-arguments.md | 2 +- docs/rules/scalar-leafs.md | 2 +- docs/rules/selection-set-depth.md | 16 +- docs/rules/strict-id-in-types.md | 119 ++++++++++ docs/rules/unique-argument-names.md | 2 +- .../unique-directive-names-per-location.md | 2 +- docs/rules/unique-directive-names.md | 2 +- docs/rules/unique-enum-value-names.md | 2 +- docs/rules/unique-field-definition-names.md | 2 +- docs/rules/unique-input-field-names.md | 2 +- docs/rules/unique-operation-types.md | 2 +- docs/rules/unique-type-names.md | 2 +- docs/rules/unique-variable-names.md | 2 +- docs/rules/value-literals-of-correct-type.md | 2 +- docs/rules/variables-are-input-types.md | 2 +- docs/rules/variables-in-allowed-position.md | 2 +- packages/plugin/src/rules/index.ts | 2 + .../plugin/src/rules/strict-id-in-types.ts | 197 ++++++++++++++++ .../plugin/tests/strict-id-in-types.spec.ts | 223 ++++++++++++++++++ 40 files changed, 628 insertions(+), 48 deletions(-) create mode 100644 .changeset/happy-trainers-doubt.md create mode 100644 docs/rules/strict-id-in-types.md create mode 100644 packages/plugin/src/rules/strict-id-in-types.ts create mode 100644 packages/plugin/tests/strict-id-in-types.spec.ts diff --git a/.changeset/happy-trainers-doubt.md b/.changeset/happy-trainers-doubt.md new file mode 100644 index 00000000000..a706acb6f6c --- /dev/null +++ b/.changeset/happy-trainers-doubt.md @@ -0,0 +1,5 @@ +--- +'@graphql-eslint/eslint-plugin': minor +--- + +[New rule] strict-id-in-types: use this to enforce output types to have a unique indentifier field unless being in exceptions diff --git a/docs/README.md b/docs/README.md index 5ff80bc40de..186dc9077a9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,5 +1,6 @@ ## Available Rules + - [`no-unreachable-types`](./rules/no-unreachable-types.md) - [`no-deprecated`](./rules/no-deprecated.md) - [`unique-fragment-name`](./rules/unique-fragment-name.md) @@ -18,6 +19,7 @@ - [`avoid-duplicate-fields`](./rules/avoid-duplicate-fields.md) - [`naming-convention`](./rules/naming-convention.md) - [`input-name`](./rules/input-name.md) +- [`strict-id-in-types`](./rules/strict-id-in-types.md) - [`prettier`](./rules/prettier.md) - [`executable-definitions`](./rules/executable-definitions.md) - [`fields-on-correct-type`](./rules/fields-on-correct-type.md) @@ -55,4 +57,4 @@ - [Writing Custom Rules](./custom-rules.md) - [How the parser works?](./parser.md) -- [`parserOptions`](./parser-options.md) +- [`parserOptions`](./parser-options.md) \ No newline at end of file diff --git a/docs/rules/executable-definitions.md b/docs/rules/executable-definitions.md index 4c666755239..ca159c7d142 100644 --- a/docs/rules/executable-definitions.md +++ b/docs/rules/executable-definitions.md @@ -7,4 +7,4 @@ A GraphQL document is only valid for execution if all definitions are either operation or fragment definitions. -> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/ExecutableDefinitionsRule.js). \ No newline at end of file +> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/ExecutableDefinitions.js). \ No newline at end of file diff --git a/docs/rules/fields-on-correct-type.md b/docs/rules/fields-on-correct-type.md index 10fcf0358ad..b11df265738 100644 --- a/docs/rules/fields-on-correct-type.md +++ b/docs/rules/fields-on-correct-type.md @@ -7,4 +7,4 @@ A GraphQL document is only valid if all fields selected are defined by the parent type, or are an allowed meta field such as __typename. -> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/FieldsOnCorrectTypeRule.js). \ No newline at end of file +> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/FieldsOnCorrectType.js). \ No newline at end of file diff --git a/docs/rules/fragments-on-composite-type.md b/docs/rules/fragments-on-composite-type.md index 8d7d31811a4..048bd84fc92 100644 --- a/docs/rules/fragments-on-composite-type.md +++ b/docs/rules/fragments-on-composite-type.md @@ -7,4 +7,4 @@ Fragments use a type condition to determine if they apply, since fragments can only be spread into a composite type (object, interface, or union), the type condition must also be a composite type. -> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/FragmentsOnCompositeTypesRule.js). \ No newline at end of file +> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/FragmentsOnCompositeTypes.js). \ No newline at end of file diff --git a/docs/rules/input-name.md b/docs/rules/input-name.md index 46d49e416d7..51447039bc5 100644 --- a/docs/rules/input-name.md +++ b/docs/rules/input-name.md @@ -50,4 +50,24 @@ The array object has the following properties: #### `checkInputType` (boolean) -Default: `"true"` \ No newline at end of file +Check that the input type name follows the convention Input + +Default: `false` + +#### `caseSensitiveInputType` (boolean) + +Allow for case discrepancies in the input type name + +Default: `true` + +#### `checkQueries` (boolean) + +Apply the rule to Queries + +Default: `false` + +#### `checkMutations` (boolean) + +Apply the rule to Mutations + +Default: `true` \ No newline at end of file diff --git a/docs/rules/known-argument-names.md b/docs/rules/known-argument-names.md index 0d3188c42dd..a34496f8c76 100644 --- a/docs/rules/known-argument-names.md +++ b/docs/rules/known-argument-names.md @@ -7,4 +7,4 @@ A GraphQL field is only valid if all supplied arguments are defined by that field. -> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/KnownArgumentNamesRule.js). \ No newline at end of file +> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/KnownArgumentNames.js). \ No newline at end of file diff --git a/docs/rules/known-directives.md b/docs/rules/known-directives.md index c372754f6da..634f33ee393 100644 --- a/docs/rules/known-directives.md +++ b/docs/rules/known-directives.md @@ -7,4 +7,4 @@ A GraphQL document is only valid if all `@directives` are known by the schema and legally positioned. -> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/KnownDirectivesRule.js). \ No newline at end of file +> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/KnownDirectives.js). \ No newline at end of file diff --git a/docs/rules/known-fragment-names.md b/docs/rules/known-fragment-names.md index c7000da2e58..1d41af6d02f 100644 --- a/docs/rules/known-fragment-names.md +++ b/docs/rules/known-fragment-names.md @@ -7,4 +7,4 @@ A GraphQL document is only valid if all `...Fragment` fragment spreads refer to fragments defined in the same document. -> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/KnownFragmentNamesRule.js). \ No newline at end of file +> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/KnownFragmentNames.js). \ No newline at end of file diff --git a/docs/rules/known-type-names.md b/docs/rules/known-type-names.md index 11750124f06..1e8a3c5335f 100644 --- a/docs/rules/known-type-names.md +++ b/docs/rules/known-type-names.md @@ -7,4 +7,4 @@ A GraphQL document is only valid if referenced types (specifically variable definitions and fragment conditions) are defined by the type schema. -> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/KnownTypeNamesRule.js). \ No newline at end of file +> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/KnownTypeNames.js). \ No newline at end of file diff --git a/docs/rules/lone-anonymous-operation.md b/docs/rules/lone-anonymous-operation.md index bcfdc29d8db..4b858631bb2 100644 --- a/docs/rules/lone-anonymous-operation.md +++ b/docs/rules/lone-anonymous-operation.md @@ -7,4 +7,4 @@ A GraphQL document is only valid if when it contains an anonymous operation (the query short-hand) that it contains only that one operation definition. -> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/LoneAnonymousOperationRule.js). \ No newline at end of file +> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/LoneAnonymousOperation.js). \ No newline at end of file diff --git a/docs/rules/lone-schema-definition.md b/docs/rules/lone-schema-definition.md index 333d10da0cb..4f3ab54c76b 100644 --- a/docs/rules/lone-schema-definition.md +++ b/docs/rules/lone-schema-definition.md @@ -7,4 +7,4 @@ A GraphQL document is only valid if it contains only one schema definition. -> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/LoneSchemaDefinitionRule.js). \ No newline at end of file +> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/LoneSchemaDefinition.js). \ No newline at end of file diff --git a/docs/rules/no-fragment-cycles.md b/docs/rules/no-fragment-cycles.md index e390d69a579..88be6f29d65 100644 --- a/docs/rules/no-fragment-cycles.md +++ b/docs/rules/no-fragment-cycles.md @@ -7,4 +7,4 @@ A GraphQL fragment is only valid when it does not have cycles in fragments usage. -> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/NoFragmentCyclesRule.js). \ No newline at end of file +> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/NoFragmentCycles.js). \ No newline at end of file diff --git a/docs/rules/no-undefined-variables.md b/docs/rules/no-undefined-variables.md index 9262311e61e..c384cb88d62 100644 --- a/docs/rules/no-undefined-variables.md +++ b/docs/rules/no-undefined-variables.md @@ -7,4 +7,4 @@ A GraphQL operation is only valid if all variables encountered, both directly and via fragment spreads, are defined by that operation. -> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/NoUndefinedVariablesRule.js). \ No newline at end of file +> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/NoUndefinedVariables.js). \ No newline at end of file diff --git a/docs/rules/no-unreachable-types.md b/docs/rules/no-unreachable-types.md index 8875e6fce68..cff14677197 100644 --- a/docs/rules/no-unreachable-types.md +++ b/docs/rules/no-unreachable-types.md @@ -3,39 +3,37 @@ - Category: `Best Practices` - Rule name: `@graphql-eslint/no-unreachable-types` - Requires GraphQL Schema: `true` [ℹ️](../../README.md#extended-linting-rules-with-graphql-schema) -- Requires GraphQL Operations: `false` [ℹ️](../../README.md#extended-linting-rules-with-siblings-operations) -This rule allow you to enforce that all types have to reachable by root level fields (Query.*, Mutation.*, Subscription.*). +Requires all types to be reachable at some level by root level fields ## Usage Examples -### Incorrect (field) +### Incorrect ```graphql # eslint @graphql-eslint/no-unreachable-types: ["error"] -type Query { - me: String +type User { + id: ID! + name: String } -type User { # This is not used, so you'll get an error - id: ID! - name: String! +type Query { + me: String } ``` - ### Correct ```graphql # eslint @graphql-eslint/no-unreachable-types: ["error"] -type Query { - me: User +type User { + id: ID! + name: String } -type User { # This is now used, so you won't get an error - id: ID! - name: String! +type Query { + me: User } ``` \ No newline at end of file diff --git a/docs/rules/no-unused-fragments.md b/docs/rules/no-unused-fragments.md index 1c9737b9f76..32f67ce3fd5 100644 --- a/docs/rules/no-unused-fragments.md +++ b/docs/rules/no-unused-fragments.md @@ -7,4 +7,4 @@ A GraphQL document is only valid if all fragment definitions are spread within operations, or spread within other fragments spread within operations. -> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/NoUnusedFragmentsRule.js). \ No newline at end of file +> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/NoUnusedFragments.js). \ No newline at end of file diff --git a/docs/rules/no-unused-variables.md b/docs/rules/no-unused-variables.md index 7a6aeb44f3d..dfac824ae46 100644 --- a/docs/rules/no-unused-variables.md +++ b/docs/rules/no-unused-variables.md @@ -7,4 +7,4 @@ A GraphQL operation is only valid if all variables defined by an operation are used, either directly or within a spread fragment. -> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/NoUnusedVariablesRule.js). \ No newline at end of file +> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/NoUnusedVariables.js). \ No newline at end of file diff --git a/docs/rules/one-field-subscriptions.md b/docs/rules/one-field-subscriptions.md index 370b2d4e0af..95b4aceebe0 100644 --- a/docs/rules/one-field-subscriptions.md +++ b/docs/rules/one-field-subscriptions.md @@ -7,4 +7,4 @@ A GraphQL subscription is valid only if it contains a single root field. -> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/SingleFieldSubscriptionsRule.js). \ No newline at end of file +> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/SingleFieldSubscriptions.js). \ No newline at end of file diff --git a/docs/rules/overlapping-fields-can-be-merged.md b/docs/rules/overlapping-fields-can-be-merged.md index 205eb3e0c7d..147187c7e06 100644 --- a/docs/rules/overlapping-fields-can-be-merged.md +++ b/docs/rules/overlapping-fields-can-be-merged.md @@ -7,4 +7,4 @@ A selection set is only valid if all fields (including spreading any fragments) either correspond to distinct response names or can be merged without ambiguity. -> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/OverlappingFieldsCanBeMergedRule.js). \ No newline at end of file +> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/OverlappingFieldsCanBeMerged.js). \ No newline at end of file diff --git a/docs/rules/possible-fragment-spread.md b/docs/rules/possible-fragment-spread.md index 581be257208..c5e7d0337e4 100644 --- a/docs/rules/possible-fragment-spread.md +++ b/docs/rules/possible-fragment-spread.md @@ -7,4 +7,4 @@ 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. -> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/PossibleFragmentSpreadsRule.js). \ No newline at end of file +> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/PossibleFragmentSpreads.js). \ No newline at end of file diff --git a/docs/rules/possible-type-extension.md b/docs/rules/possible-type-extension.md index 6d8c0998124..88b174c73d1 100644 --- a/docs/rules/possible-type-extension.md +++ b/docs/rules/possible-type-extension.md @@ -7,4 +7,4 @@ A type extension is only valid if the type is defined and has the same kind. -> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/PossibleTypeExtensionsRule.js). \ No newline at end of file +> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/PossibleTypeExtensions.js). \ No newline at end of file diff --git a/docs/rules/provided-required-arguments.md b/docs/rules/provided-required-arguments.md index d8c6991f28e..5af29c9312d 100644 --- a/docs/rules/provided-required-arguments.md +++ b/docs/rules/provided-required-arguments.md @@ -7,4 +7,4 @@ A field or directive is only valid if all required (non-null without a default value) field arguments have been provided. -> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/ProvidedRequiredArgumentsRule.js). \ No newline at end of file +> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/ProvidedRequiredArguments.js). \ No newline at end of file diff --git a/docs/rules/scalar-leafs.md b/docs/rules/scalar-leafs.md index 87ebaebfc3d..6e62c2e1fc9 100644 --- a/docs/rules/scalar-leafs.md +++ b/docs/rules/scalar-leafs.md @@ -7,4 +7,4 @@ A GraphQL document is valid only if all leaf fields (fields without sub selections) are of scalar or enum types. -> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/ScalarLeafsRule.js). \ No newline at end of file +> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/ScalarLeafs.js). \ No newline at end of file diff --git a/docs/rules/selection-set-depth.md b/docs/rules/selection-set-depth.md index 7b37fb59ec4..6f56e737e1d 100644 --- a/docs/rules/selection-set-depth.md +++ b/docs/rules/selection-set-depth.md @@ -53,4 +53,18 @@ query deep2 { ## Config Schema -The schema defines the following properties: \ No newline at end of file +### (array) + +The schema defines an array with all elements of the type `object`. + +The array object has the following properties: + +#### `maxDepth` (number) + +#### `ignore` (array) + +The object is an array with all elements of the type `string`. + +Additional restrictions: + +* Minimum items: `1` \ No newline at end of file diff --git a/docs/rules/strict-id-in-types.md b/docs/rules/strict-id-in-types.md new file mode 100644 index 00000000000..84e07a55779 --- /dev/null +++ b/docs/rules/strict-id-in-types.md @@ -0,0 +1,119 @@ +# `strict-id-in-types` + +- Category: `Best practices` +- Rule name: `@graphql-eslint/strict-id-in-types` +- Requires GraphQL Schema: `false` [ℹ️](../../README.md#extended-linting-rules-with-graphql-schema) +- Requires GraphQL Operations: `false` [ℹ️](../../README.md#extended-linting-rules-with-siblings-operations) + +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. + +## Usage Examples + +### Incorrect + +```graphql +# eslint @graphql-eslint/strict-id-in-types: ["error", [{"acceptedIdNames":["id","_id"],"acceptedIdTypes":["ID"],"exceptions":{"suffixes":["Payload"]}}]] + +# Incorrect field name +type InvalidFieldName { + key: ID! +} + +# Incorrect field type +type InvalidFieldType { + id: String! +} + +# Incorrect exception suffix +type InvalidSuffixResult { + data: String! +} + +# Too many unique identifiers. Must only contain one. +type InvalidFieldName { + id: ID! + _id: ID! +} +``` + +### Correct + +```graphql +# eslint @graphql-eslint/strict-id-in-types: ["error", [{"acceptedIdNames":["id","_id"],"acceptedIdTypes":["ID"],"exceptions":{"types":["Error"],"suffixes":["Payload"]}}]] + +type User { + id: ID! +} + +type Post { + _id: ID! +} + +type CreateUserPayload { + data: String! +} + +type Error { + message: String! +} +``` + +## Config Schema + +### (array) + +The schema defines an array with all elements of the type `object`. + +The array object has the following properties: + +#### `acceptedIdNames` (array) + +The object is an array with all elements of the type `string`. + +Default: + +``` +[ + "id" +] +``` + +#### `acceptedIdTypes` (array) + +The object is an array with all elements of the type `string`. + +Default: + +``` +[ + "ID" +] +``` + +#### `exceptions` (object) + +Properties of the `exceptions` object: + +##### `types` (array) + +This is used to exclude types with names that match one of the specified values. + +The object is an array with all elements of the type `string`. + +Default: + +``` +[] +``` + +##### `suffixes` (array) + +This is used to exclude types with names with suffixes that match one of the specified values. + +The object is an array with all elements of the type `string`. + +Default: + +``` +[] +``` \ No newline at end of file diff --git a/docs/rules/unique-argument-names.md b/docs/rules/unique-argument-names.md index 9878b6be3d1..9ddab04bcad 100644 --- a/docs/rules/unique-argument-names.md +++ b/docs/rules/unique-argument-names.md @@ -7,4 +7,4 @@ A GraphQL field or directive is only valid if all supplied arguments are uniquely named. -> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/UniqueArgumentNamesRule.js). \ No newline at end of file +> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/UniqueArgumentNames.js). \ No newline at end of file diff --git a/docs/rules/unique-directive-names-per-location.md b/docs/rules/unique-directive-names-per-location.md index fddbabd192c..8dd9deba5f5 100644 --- a/docs/rules/unique-directive-names-per-location.md +++ b/docs/rules/unique-directive-names-per-location.md @@ -7,4 +7,4 @@ A GraphQL document is only valid if all non-repeatable directives at a given location are uniquely named. -> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/UniqueDirectivesPerLocationRule.js). \ No newline at end of file +> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/UniqueDirectivesPerLocation.js). \ No newline at end of file diff --git a/docs/rules/unique-directive-names.md b/docs/rules/unique-directive-names.md index 8e548aefe7c..3da42d041b9 100644 --- a/docs/rules/unique-directive-names.md +++ b/docs/rules/unique-directive-names.md @@ -7,4 +7,4 @@ A GraphQL document is only valid if all defined directives have unique names. -> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/UniqueDirectiveNamesRule.js). \ No newline at end of file +> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/UniqueDirectiveNames.js). \ No newline at end of file diff --git a/docs/rules/unique-enum-value-names.md b/docs/rules/unique-enum-value-names.md index 73a7543c032..92ab3c1a728 100644 --- a/docs/rules/unique-enum-value-names.md +++ b/docs/rules/unique-enum-value-names.md @@ -7,4 +7,4 @@ A GraphQL enum type is only valid if all its values are uniquely named. -> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/UniqueEnumValueNamesRule.js). \ No newline at end of file +> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/UniqueEnumValueNames.js). \ No newline at end of file diff --git a/docs/rules/unique-field-definition-names.md b/docs/rules/unique-field-definition-names.md index 076efbdbdb2..b0426dfec89 100644 --- a/docs/rules/unique-field-definition-names.md +++ b/docs/rules/unique-field-definition-names.md @@ -7,4 +7,4 @@ A GraphQL complex type is only valid if all its fields are uniquely named. -> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/UniqueFieldDefinitionNamesRule.js). \ No newline at end of file +> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/UniqueFieldDefinitionNames.js). \ No newline at end of file diff --git a/docs/rules/unique-input-field-names.md b/docs/rules/unique-input-field-names.md index 99c9381cf9e..5f331acfcb1 100644 --- a/docs/rules/unique-input-field-names.md +++ b/docs/rules/unique-input-field-names.md @@ -7,4 +7,4 @@ A GraphQL input object value is only valid if all supplied fields are uniquely named. -> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/UniqueInputFieldNamesRule.js). \ No newline at end of file +> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/UniqueInputFieldNames.js). \ No newline at end of file diff --git a/docs/rules/unique-operation-types.md b/docs/rules/unique-operation-types.md index 00fd1adfe37..bdc52db6880 100644 --- a/docs/rules/unique-operation-types.md +++ b/docs/rules/unique-operation-types.md @@ -7,4 +7,4 @@ A GraphQL document is only valid if it has only one type per operation. -> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/UniqueOperationTypesRule.js). \ No newline at end of file +> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/UniqueOperationTypes.js). \ No newline at end of file diff --git a/docs/rules/unique-type-names.md b/docs/rules/unique-type-names.md index eec17cc1123..8759ffff06e 100644 --- a/docs/rules/unique-type-names.md +++ b/docs/rules/unique-type-names.md @@ -7,4 +7,4 @@ A GraphQL document is only valid if all defined types have unique names. -> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/UniqueTypeNamesRule.js). \ No newline at end of file +> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/UniqueTypeNames.js). \ No newline at end of file diff --git a/docs/rules/unique-variable-names.md b/docs/rules/unique-variable-names.md index cfde7000149..5aca0a5c1d7 100644 --- a/docs/rules/unique-variable-names.md +++ b/docs/rules/unique-variable-names.md @@ -7,4 +7,4 @@ A GraphQL operation is only valid if all its variables are uniquely named. -> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/UniqueVariableNamesRule.js). \ No newline at end of file +> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/UniqueVariableNames.js). \ No newline at end of file diff --git a/docs/rules/value-literals-of-correct-type.md b/docs/rules/value-literals-of-correct-type.md index 39ebfe8ca6c..6302b40f8ad 100644 --- a/docs/rules/value-literals-of-correct-type.md +++ b/docs/rules/value-literals-of-correct-type.md @@ -7,4 +7,4 @@ A GraphQL document is only valid if all value literals are of the type expected at their position. -> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/ValuesOfCorrectTypeRule.js). \ No newline at end of file +> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/ValuesOfCorrectType.js). \ No newline at end of file diff --git a/docs/rules/variables-are-input-types.md b/docs/rules/variables-are-input-types.md index ecf59776451..757a713085c 100644 --- a/docs/rules/variables-are-input-types.md +++ b/docs/rules/variables-are-input-types.md @@ -7,4 +7,4 @@ A GraphQL operation is only valid if all the variables it defines are of input types (scalar, enum, or input object). -> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/VariablesAreInputTypesRule.js). \ No newline at end of file +> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/VariablesAreInputTypes.js). \ No newline at end of file diff --git a/docs/rules/variables-in-allowed-position.md b/docs/rules/variables-in-allowed-position.md index 4a82085dc01..2f4afee1d27 100644 --- a/docs/rules/variables-in-allowed-position.md +++ b/docs/rules/variables-in-allowed-position.md @@ -7,4 +7,4 @@ Variables passed to field arguments conform to type. -> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/VariablesInAllowedPositionRule.js). \ No newline at end of file +> This rule is a wrapper around a `graphql-js` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/master/src/validation/rules/VariablesInAllowedPosition.js). \ No newline at end of file diff --git a/packages/plugin/src/rules/index.ts b/packages/plugin/src/rules/index.ts index 0ffb8563e08..acf6f700e18 100644 --- a/packages/plugin/src/rules/index.ts +++ b/packages/plugin/src/rules/index.ts @@ -17,6 +17,7 @@ import noDeprecated from './no-deprecated'; import noHashtagDescription from './no-hashtag-description'; import selectionSetDepth from './selection-set-depth'; import avoidDuplicateFields from './avoid-duplicate-fields'; +import strictIdInTypes from './strict-id-in-types'; import { GraphQLESLintRule } from '../types'; import { GRAPHQL_JS_VALIDATIONS } from './graphql-js-validation'; @@ -39,6 +40,7 @@ export const rules: Record = { 'avoid-duplicate-fields': avoidDuplicateFields, 'naming-convention': namingConvention, 'input-name': inputName, + 'strict-id-in-types': strictIdInTypes, prettier, ...GRAPHQL_JS_VALIDATIONS, }; diff --git a/packages/plugin/src/rules/strict-id-in-types.ts b/packages/plugin/src/rules/strict-id-in-types.ts new file mode 100644 index 00000000000..944844d511b --- /dev/null +++ b/packages/plugin/src/rules/strict-id-in-types.ts @@ -0,0 +1,197 @@ +import { Kind, ObjectTypeDefinitionNode } from 'graphql'; +import { GraphQLESTreeNode } from '../estree-parser'; +import { GraphQLESLintRule } from '../types'; + +interface ExceptionRule { + types?: string[]; + suffixes?: string[]; +} + +type StrictIdInTypesRuleConfig = [ + { + acceptedIdNames?: string[]; + acceptedIdTypes?: string[]; + exceptions?: ExceptionRule; + } +]; + +interface ShouldIgnoreNodeParams { + node: GraphQLESTreeNode; + exceptions: ExceptionRule; +} +const shouldIgnoreNode = ({ node, exceptions }: ShouldIgnoreNodeParams): boolean => { + const rawNode = node.rawNode(); + + if (exceptions.types && exceptions.types.some(type => rawNode.name.value === type)) { + return true; + } + + if (exceptions.suffixes && exceptions.suffixes.some(suffix => rawNode.name.value.endsWith(suffix))) { + return true; + } + + return false; +}; + +const rule: GraphQLESLintRule = { + meta: { + type: 'suggestion', + docs: { + description: + '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.', + category: 'Best practices', + recommended: true, + requiresSchema: false, + requiresSiblings: false, + url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/strict-id-in-types.md', + examples: [ + { + title: 'Incorrect', + usage: [{ acceptedIdNames: ['id', '_id'], acceptedIdTypes: ['ID'], exceptions: { suffixes: ['Payload'] } }], + code: /* GraphQL */ ` + # Incorrect field name + type InvalidFieldName { + key: ID! + } + + # Incorrect field type + type InvalidFieldType { + id: String! + } + + # Incorrect exception suffix + type InvalidSuffixResult { + data: String! + } + + # Too many unique identifiers. Must only contain one. + type InvalidFieldName { + id: ID! + _id: ID! + } + `, + }, + { + title: 'Correct', + usage: [ + { + acceptedIdNames: ['id', '_id'], + acceptedIdTypes: ['ID'], + exceptions: { types: ['Error'], suffixes: ['Payload'] }, + }, + ], + code: /* GraphQL */ ` + type User { + id: ID! + } + + type Post { + _id: ID! + } + + type CreateUserPayload { + data: String! + } + + type Error { + message: String! + } + `, + }, + ], + }, + schema: { + $schema: 'http://json-schema.org/draft-04/schema#', + type: 'array', + items: { + type: 'object', + properties: { + acceptedIdNames: { + type: 'array', + items: { + type: 'string', + }, + default: ['id'], + }, + acceptedIdTypes: { + type: 'array', + items: { + type: 'string', + }, + default: ['ID'], + }, + exceptions: { + type: 'object', + properties: { + types: { + type: 'array', + description: 'This is used to exclude types with names that match one of the specified values.', + items: { + type: 'string', + }, + default: [], + }, + suffixes: { + type: 'array', + description: + 'This is used to exclude types with names with suffixes that match one of the specified values.', + items: { + type: 'string', + }, + default: [], + }, + }, + }, + }, + }, + }, + }, + create(context) { + const options: StrictIdInTypesRuleConfig[number] = { + acceptedIdNames: ['id'], + acceptedIdTypes: ['ID'], + exceptions: {}, + ...(context.options[0] || {}), + }; + + return { + ObjectTypeDefinition(node) { + if (shouldIgnoreNode({ node, exceptions: options.exceptions })) { + return; + } + + const validIds = node.fields.filter(field => { + const fieldNode = field.rawNode(); + + const isValidIdName = options.acceptedIdNames.includes(fieldNode.name.value); + + // To be a valid type, it must be non-null and one of the accepted types. + let isValidIdType = false; + if (fieldNode.type.kind === Kind.NON_NULL_TYPE && fieldNode.type.type.kind === Kind.NAMED_TYPE) { + isValidIdType = options.acceptedIdTypes.includes(fieldNode.type.type.name.value); + } + + return isValidIdName && isValidIdType; + }); + + // Usually, there should be only one unique identifier field per type. + // Some clients allow multiple fields to be used. If more people need this, + // we can extend this rule later. + if (validIds.length !== 1) { + context.report({ + node, + message: + '{{nodeName}} must have exactly one non-nullable unique identifier. Accepted name(s): {{acceptedNamesString}} ; Accepted type(s): {{acceptedTypesString}}', + data: { + nodeName: node.name.value, + acceptedNamesString: options.acceptedIdNames.join(','), + acceptedTypesString: options.acceptedIdTypes.join(','), + }, + }); + } + }, + }; + }, +}; + +export default rule; diff --git a/packages/plugin/tests/strict-id-in-types.spec.ts b/packages/plugin/tests/strict-id-in-types.spec.ts new file mode 100644 index 00000000000..6a97944137b --- /dev/null +++ b/packages/plugin/tests/strict-id-in-types.spec.ts @@ -0,0 +1,223 @@ +import { GraphQLRuleTester } from '../src/testkit'; +import rule from '../src/rules/strict-id-in-types'; + +const ruleTester = new GraphQLRuleTester(); + +ruleTester.runGraphQLTests('strict-id-in-types', rule, { + valid: [ + { + code: 'type A { id: ID! }', + }, + { + code: 'type A { _id: String! }', + options: [ + { + acceptedIdNames: ['_id'], + acceptedIdTypes: ['String'], + }, + ], + }, + { + code: 'type A { _id: String! } type A1 { id: ID! }', + options: [ + { + acceptedIdNames: ['id', '_id'], + acceptedIdTypes: ['ID', 'String'], + }, + ], + }, + { + code: 'type A { id: ID! } type AResult { key: String! } ', + options: [ + { + acceptedIdNames: ['id'], + acceptedIdTypes: ['ID'], + exceptions: { + suffixes: ['Result'], + }, + }, + ], + }, + { + code: 'type A { id: ID! } type A1 { id: ID! } ', + options: [ + { + acceptedIdNames: ['id'], + acceptedIdTypes: ['ID'], + exceptions: { + suffixes: [''], + }, + }, + ], + }, + { + code: 'type A { id: ID! } type A1 { id: ID! } ', + options: [ + { + acceptedIdNames: ['id'], + acceptedIdTypes: ['ID'], + exceptions: { + suffixes: [], + }, + }, + ], + }, + { + code: + 'type A { id: ID! } type AResult { key: String! } type APayload { bool: Boolean! } type APagination { num: Int! }', + options: [ + { + acceptedIdNames: ['id'], + acceptedIdTypes: ['ID'], + exceptions: { + suffixes: ['Result', 'Payload', 'Pagination'], + }, + }, + ], + }, + { + code: 'type A { id: ID! } type AError { message: String! }', + options: [ + { + acceptedIdNames: ['id'], + acceptedIdTypes: ['ID'], + exceptions: { + types: ['AError'], + }, + }, + ], + }, + { + code: 'type A { id: ID! } type AGeneralError { message: String! } type AForbiddenError { message: String! }', + options: [ + { + acceptedIdNames: ['id'], + acceptedIdTypes: ['ID'], + exceptions: { + types: ['AGeneralError', 'AForbiddenError'], + }, + }, + ], + }, + { + code: 'type A { id: ID! }', + options: [ + { + acceptedIdNames: ['id'], + acceptedIdTypes: ['ID'], + exceptions: { + types: [''], + }, + }, + ], + }, + { + code: 'type A { id: ID! } type AError { message: String! } type AResult { payload: A! }', + options: [ + { + acceptedIdNames: ['id'], + acceptedIdTypes: ['ID'], + exceptions: { + types: ['AError'], + suffixes: ['Result'], + }, + }, + ], + }, + ], + invalid: [ + { + code: 'type B { name: String! }', + errors: [ + { + message: + 'B must have exactly one non-nullable unique identifier. Accepted name(s): id ; Accepted type(s): ID', + }, + ], + }, + { + code: 'type B { id: ID! _id: String! }', + options: [ + { + acceptedIdNames: ['id', '_id'], + acceptedIdTypes: ['ID', 'String'], + }, + ], + errors: [ + { + message: + 'B must have exactly one non-nullable unique identifier. Accepted name(s): id,_id ; Accepted type(s): ID,String', + }, + ], + }, + { + code: + 'type B { id: String! } type B1 { id: [String] } type B2 { id: [String!] } type B3 { id: [String]! } type B4 { id: [String!]! }', + options: [ + { + acceptedIdNames: ['id'], + acceptedIdTypes: ['String'], + }, + ], + errors: [ + { + message: + 'B1 must have exactly one non-nullable unique identifier. Accepted name(s): id ; Accepted type(s): String', + }, + { + message: + 'B2 must have exactly one non-nullable unique identifier. Accepted name(s): id ; Accepted type(s): String', + }, + { + message: + 'B3 must have exactly one non-nullable unique identifier. Accepted name(s): id ; Accepted type(s): String', + }, + { + message: + 'B4 must have exactly one non-nullable unique identifier. Accepted name(s): id ; Accepted type(s): String', + }, + ], + }, + { + code: + 'type B { id: ID! } type Bresult { key: String! } type BPayload { bool: Boolean! } type BPagination { num: Int! }', + options: [ + { + acceptedIdNames: ['id'], + acceptedIdTypes: ['ID'], + exceptions: { + suffixes: ['Result', 'Payload'], + }, + }, + ], + errors: [ + { + message: + 'Bresult must have exactly one non-nullable unique identifier. Accepted name(s): id ; Accepted type(s): ID', + }, + { + message: + 'BPagination must have exactly one non-nullable unique identifier. Accepted name(s): id ; Accepted type(s): ID', + }, + ], + }, + { + code: 'type B { id: ID! } type BError { message: String! }', + options: [ + { + acceptedIdNames: ['id'], + acceptedIdTypes: ['ID'], + exceptions: { + types: ['GeneralError'], + }, + }, + ], + errors: [ + { + message: + 'BError must have exactly one non-nullable unique identifier. Accepted name(s): id ; Accepted type(s): ID', + }, + ], + }, + ], +});