@rabotaua/graphql-schema-linter
v1.0.20
Published
Validates GraphQL schema against rules
Downloads
7
Readme
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
torules
andcustomRulePaths
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
}