npm package: @rabotaua/graphql-schema-linter
Description: Validates GraphQL schema against rules
Author: Alexandr Marchenko
Latest version: 1.0.20

# Quick start

```bash
npx @rabotaua/graphql-schema-linter http://localhost:4000/graphql
```

# Not automated rules

Rules that were not automated are [here](https://rabota.atlassian.net/wiki/spaces/GQL/pages/1840513025/GraphQL+Rules)

# Wanted rules

- namespaced mutations [link](https://graphql-rules.com/rules/mutation-namespaces)
- resolvers returning array has filter argument - connections instead
- more than X% of arguments are optional
- mutations have single input argument
- booleans are prefixed - does not check inputs

# Wanted fixes

- duplicate checks should not touch interfaces
- duplicate checks should not touch deprecated
- deprecated should have deadline

# Adding new rule

- create rule and spec (just copy one from rules folder)
- make sure that tests are working, e.g. `node_modules/.bin/mocha rules/my-rule.spec.js`
- add it to `graphql-schema-linter.config.js` to `rules` and `customRulePaths` sections
- run it against graph, e.g. `node cli.js https://graph.exmple.com/graphql`
- to not harm add found errors to `assets/snapshot.js` and then to config
- add examples and description to readme
- revert `mutations-starts-with-verb` after moving to github

# Opting out

In case some rule can not be applied just add it to `graphql-schema-linter.config.js`

# Rules

## arguments-have-descriptions

Descriptions are must have, otherwise it will become a blackbox

```gql
type Query {
    getCityWithLessThan(num: Int!) [City!]! # BAD - What the hell `num` stands for?!
    
    # GOOD
    getCityWithLessThan(
        "Number of population"
        num: Int!
    ) [City!]!
}
```

Yep, still something strange but at least anyone can answer the question

## defined-types-are-used

Ensures that defined types are accessible

### Invalid

```gql
type City { id: ID! }

type Country { id: ID! }

type Query { cities: [City!]! }
```

Country type is not accessible in any way

### Valid

```gql
type City { id: ID! country: Country }

type Country { id: ID! }

type Query { cities: [City!]! }
```

Country is accessible through cities in main query

> Do you remember issues when we can not check graph because of empty mutation type

## deprecations-have-a-reason

Requires deprecations to have a comment

It is so hard to answers why something was deprecated three month ago

### Invalid

```gql
type Country @deprecated { id: ID! }
```

Why country is deprecated at all?!

### Valid

```gql
type Country @deprecated(reason: "Everything is globalized, will be removed from graph at 2022 Q4, use Planet type instead") { id: ID! }
```

Ok, at least we know the answer

## descriptions-are-capitalized

It is just make sure that we are not blindly copy-pasting field names to their descriptions

### Invalid

```gql
type Country {
    "id" 
    id: ID!
}
```

**DO NOT DO THAT!** (TODO: create dedicated rule if there is no one already)

### Valid

```gql
type Country {
    "Country identifier, strign abbreviation, ISO, examples: ru, uk" 
    id: ID!
}
```

Even simple fields can have description, yes, not always but at least it will be a placeholder for future notes

## enum-values-all-caps

Ensures that enums will be consistent

### Invalid

```gql
enum Language {
    Urk
    Rus
}
```

Why not lower cased, or may be snake cased 🤔

### Valid

```gql
enum Language {
    UA
    RU
}
```

Ok, we have standard

## enum-values-have-descriptions

The same as attributes, everything should be described

### Invalid

```gql
enum Lang {
    UK
}
```

Does `UK` stands for the United Kingdom or Ukraine?

### Valid

```gql
enum Lang {
    "Ukraine, ISO standard for language is UK and for country UA"
    UK
}
```

## fields-are-camel-cased

Field names should be consistent, otherwise it will become a nightmare

### Invalid

```gql
type City {
    id: ID!
    Name: String!
    iSOCode: String!
    country_id: ID!
}
```

WTF?!

### Valid

```gql
type City {
    id: ID!
    name: String!
    code: String!
    country: Country!
}
```

Yes, not always it can be nice, but try to choose readable names and make them `camelCased`

## fields-have-descriptions

The same story again, descriptions are needed, otherwise we will have black box

### Invalid

```gql
type City {
    # ...
    code: String!
    # ...
}
```

Code is - ?!?!

### Valid

```gql
type City {
    "ISO code for a country, e.g. UA for Ukraine, UK for Unighted Kingdom"
    code: String!
    # ...
}
```

Yep, it should be enum, but at least better than nothing

## input-object-values-are-camel-cased

Not only types, but input fields should also have standard

### Invalid

```gql
input GetCitiesInput {
    NameContains: String!,
    id: ID,
    population_less_than: Int
}

type Query {
    getCities(input: GetCitiesInput!): [City!]!
}
```

WTF?!

### Valid

```gql
input GetCitiesInput {
    nameContains: String!,
    id: ID,
    populationLessThan: Int
}

type Query {
    getCities(input: GetCitiesInput!): [City!]!
}
```

Yep, it should already be an input, and population should have deeper inputs but still this is at least consistent

## input-object-values-have-descriptions

The same rule for descriptions here

### Invalid

```gql
input GetCitiesInput {
    num: Int
}

type Query {
    getCities(input: GetCitiesInput!): [City!]!
}
```

`num` stands for what?!

### Valid

```gql
input GetCitiesInput {
    "Number of population"
    num: Int
}

type Query {
    getCities(input: GetCitiesInput!): [City!]!
}
```

At least it becomes little-bit clear, and what is more important it is a placeholder for future descriptions

## relay-connection-types-spec

Build in check to ensure that we are not reinventing wheel with paged results

## relay-connection-arguments-spec

Build in check to ensure that we are not reinventing wheel with paged results

## types-are-capitalized

Ensures that type names are consistent

### Invalid

```gql
type Country {}
type city {}
type country_languege {}
```

WTF?!

### Valid

```gql
type Country {}
type City {}
type CountryLanguege {}
```

### types-have-descriptions

The same story again and again, everything should be described

### Invalid

```gql
type Ticket {}
```

What is `Ticket`?!

### Valid

```gql
"Ticket is our previous ordering system, and is being replaced by Order"
type Ticked {}
```

At least better, do not forget do deprecate such things

## identifiers-are-connected

Clients are not interested of identifiers, it is a code smell in graphql world

### Invalid

```gql
type City {
    id: ID!
    name: String!
    countryId: ID!
}
```

Why do I need `countryId` at all?!

### Valid

```gql
type City {
    id: ID!
    name: String!
    country: Country!
}
```

Ok, it is better, if I really need only id I can retrieve it, but graph becomes "hairy" and if I need something else it will be here

## enums-are-prefixed

It will be so much easier to identify them

### Invalid

```gql
type Employee {
    role: EmployeeRole
}
```

Is `EmployeeRole` type or enum?

### Valid

```gql
type Employee {
    role: EmployeeRoleEnum
}
```

## inputs-are-prefixed

It will be so much easier to identify them

### Invalid

```gql
input Employee {
    # ...
}
```

Is `Employee` type or input?

### Valid

```gql
input EmployeeInput {
    # ...
}
```

## identifiers-are-required

How can entity have no identifier?

### Invalid

```gql
type City {
    id: ID
}
```

### Valid

```gql
type City {
    id: ID!
}
```

## lists-are-required

Defining a field in your schema as NonNull means that GraphQL promises to always return a value when the field is queried. It allows clients to do fewer response validation checks in their code and improves static analysis. Event if backend does not return data on a required (NonNull) field, GraphQL will return an error stating that there is no data. In this case, the parent object value will be set to null. If the parent object is also a required field (NonNull) then the error will propagate higher. In any case, the consumer will not receive an object (GraphQL type) without a data for a required (NonNull) field.

### Invalid

```gql
type MyLists {
  list1: [String] # [], [null], null
  list2: [String]! # [], [null]
  list3: [String!] # [], null
}
```

### Valid

```gql
type MyLists {
  list4: [String!]! # []                <-- BETTER!
}
```

## dates-are-explicit

Try using a stricter type for input data. For example, define a new scalar type DateTime instead of using a String. As you might know, GraphQL has 5 built-in scalar types and date isn't one of them. However, GraphQL allows to create a custom scalar type with type description and implement your own type validation, serialization and deserialization rules.

### Invalid

```gql
type Event {
    date: String
}
```

### Valid

```gql
type Event {
    date: Date
}
```

## fields-are-required

If types has many optional fields it might become a problem, and probably it should be divided into different types, in general you should try to avoid optionals as much as possible

### Invalid

```gql
type Account {
    id: ID!
    username: String
    age: Int
    skills: [String]
}
```

This will force bazillion of null reference checks on clients

### Valid

```gql
type Account {
    id: ID!
    username: String!
    age: Int!
    skills: [String!]!
}
```

## arguments-are-tiny

Did you ever see a method with twenty bool arguments? If yes no more description needed

### Valid

```gql
type Inventory {
    products(input: ProductsInput): [Product!]!
}
```

### Invalid

```gql
type Inventory {
    sale(priceFrom: Int, priceTo: Int, onSale: Boolean, inStock: Boolean, titleContains: String, shippable: Boolean): [Product]!
}
```

## booleans-are-prefixed

Be more precise in naming, booleans should be prefixed with `is` or `has` to make it clear

### Valid

```gql
type User {
    isLoggedIn: Boolean!
    hasConfirmedEmail: Boolean!
}
```

### Invalid

```gql
type User {
    logged: Boolean
    emailConfirmed: Boolean!
}
```

## mutations-starts-with-verb

To make it clear each mutation should start with verb

### Invalid

```gql
type Mutation {
    productCreate: Product
    product: Product
}
```

### Valid

```gql
type Mutation {
    createProduct: Product
    updateProduct: Product
}
```

## types-are-spellchecked

Be gentle and write correct names

### Invalid

```gql
type Kadabra {
    id: ID
}
```

### Valid

```gql
type Resume {
    id: ID
}
```

## types-are-deduplicated

DRY

```gql
type Article {
    id: ID!
    title: String!
    content: String!
}

...

# Invalid
type Post {
    id: ID!
    title: String!
    content: String!
}
```

## identifiers-are-identifiers

There is a dedicated type `ID` for identifiers

### Valid

```gql
type City {
    id: ID!
    countryId: ID!
}
```

### Invalid

```gql
type City {
    id: Int
    countryId: Int
}
```