1.0.20 • Published 3 years ago

@rabotaua/graphql-schema-linter v1.0.20

Weekly downloads
-
License
MIT
Repository
github
Last release
3 years ago

Quick start

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

Not automated rules

Rules that were not automated are here

Wanted rules

  • namespaced mutations link
  • 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

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

type City { id: ID! }

type Country { id: ID! }

type Query { cities: [City!]! }

Country type is not accessible in any way

Valid

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

type Country @deprecated { id: ID! }

Why country is deprecated at all?!

Valid

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

type Country {
    "id" 
    id: ID!
}

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

Valid

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

enum Language {
    Urk
    Rus
}

Why not lower cased, or may be snake cased 🤔

Valid

enum Language {
    UA
    RU
}

Ok, we have standard

enum-values-have-descriptions

The same as attributes, everything should be described

Invalid

enum Lang {
    UK
}

Does UK stands for the United Kingdom or Ukraine?

Valid

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

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

WTF?!

Valid

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

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

Code is - ?!?!

Valid

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

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

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

WTF?!

Valid

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

input GetCitiesInput {
    num: Int
}

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

num stands for what?!

Valid

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

type Country {}
type city {}
type country_languege {}

WTF?!

Valid

type Country {}
type City {}
type CountryLanguege {}

types-have-descriptions

The same story again and again, everything should be described

Invalid

type Ticket {}

What is Ticket?!

Valid

"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

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

Why do I need countryId at all?!

Valid

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

type Employee {
    role: EmployeeRole
}

Is EmployeeRole type or enum?

Valid

type Employee {
    role: EmployeeRoleEnum
}

inputs-are-prefixed

It will be so much easier to identify them

Invalid

input Employee {
    # ...
}

Is Employee type or input?

Valid

input EmployeeInput {
    # ...
}

identifiers-are-required

How can entity have no identifier?

Invalid

type City {
    id: ID
}

Valid

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

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

Valid

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

type Event {
    date: String
}

Valid

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

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

This will force bazillion of null reference checks on clients

Valid

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

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

Invalid

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

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

Invalid

type User {
    logged: Boolean
    emailConfirmed: Boolean!
}

mutations-starts-with-verb

To make it clear each mutation should start with verb

Invalid

type Mutation {
    productCreate: Product
    product: Product
}

Valid

type Mutation {
    createProduct: Product
    updateProduct: Product
}

types-are-spellchecked

Be gentle and write correct names

Invalid

type Kadabra {
    id: ID
}

Valid

type Resume {
    id: ID
}

types-are-deduplicated

DRY

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

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

Invalid

type City {
    id: Int
    countryId: Int
}
1.0.20

3 years ago

1.0.19

3 years ago

1.0.18

3 years ago

1.0.17

3 years ago

1.0.16

3 years ago

1.0.15

3 years ago

1.0.14

3 years ago

1.0.13

3 years ago

1.0.12

3 years ago

1.0.11

3 years ago

1.0.10

3 years ago

1.0.9

3 years ago

1.0.8

3 years ago

1.0.7

3 years ago

1.0.6

3 years ago

1.0.5

3 years ago

1.0.4

3 years ago

1.0.3

3 years ago

1.0.2

3 years ago

1.0.1

3 years ago

1.0.0

3 years ago