Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/graphql-sdl-linting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@redocly/openapi-core': minor
'@redocly/cli': minor
---

Added experimental support for linting GraphQL SDL schema files (`.graphql` / `.gql`).
5 changes: 3 additions & 2 deletions docs/@v2/commands/lint.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

## Introduction

Redocly CLI can identify and report on problems found in OpenAPI, AsyncAPI, Arazzo, or Open-RPC descriptions.
This helps you avoid bugs and make API or Arazzo descriptions more consistent.
Redocly CLI can identify and report on problems found in OpenAPI, AsyncAPI, or Arazzo descriptions.
It also has experimental support for [Open-RPC](../guides/lint-openrpc.md) and [GraphQL SDL schemas](../guides/lint-graphql.md).
This helps you avoid bugs and make API descriptions more consistent.

The `lint` command reports on problems and executes preprocessors and rules.
Unlike the `bundle` command, `lint` doesn't execute decorators.
Expand Down
86 changes: 86 additions & 0 deletions docs/@v2/guides/lint-graphql.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
---
seo:
title: Lint GraphQL with Redocly CLI
description: Use the Redocly CLI to validate GraphQL SDL schemas, or configure rules for GraphQL.
---

# Lint GraphQL with Redocly CLI

{% admonition type="warning" name="Experimental" %}
This is an experimental feature. Its behavior may change in future releases.
{% /admonition %}

In addition to providing lint functionality for multiple OpenAPI formats, Redocly CLI also supports GraphQL.
Redocly CLI supports the following linting approaches with GraphQL documents:

- GraphQL SDL syntax and schema validation
- built-in rules for checking common standards requirements (see the [list of GraphQL rules](#graphql-rules))
- [configurable rules](../rules/configurable-rules.md) for building rules following common patterns

## Lint an existing GraphQL file

Redocly CLI takes its settings from a `redocly.yaml` configuration file.
The following is an example of a simple configuration file that checks if a GraphQL SDL file is well-formed and has a valid structure:

```yaml
rules:
struct: error
```

The `struct` rule reports an error if the SDL contains a syntax error or if the schema is structurally invalid (for example, a field that references a type that isn't defined).

With this configuration file, and your GraphQL schema file, run the linting command:

```sh
redocly lint schema.graphql
```

The output describes structural problems with the document, or reports that it is valid.

{% admonition type="info" name="Syntax errors always stop linting" %}
A GraphQL syntax error is always reported as an error and ends linting for that file, regardless of how `struct` is configured.
{% /admonition %}

## GraphQL rules

To expand the linting checks for a GraphQL schema, enable the built-in GraphQL rules.
Unlike the shared `struct` rule (configured under `rules`), GraphQL-specific built-in rules are configured under the `graphqlRules` section.
The supported rules are:

- `no-unused-types`: Every declared type must be referenced by another type or directive, or serve as a root operation type.
Root types are the ones named in the `schema` definition or its extensions; when there is no `schema` definition, types named `Query`, `Mutation`, or `Subscription` are the roots.
Types that are declared but never referenced are reported.
If the document has no root operation type, this rule reports nothing.
- `type-description`: Every type definition (object, interface, enum, input object, union, and scalar) must have a non-empty description.

We expect the list to expand over time, so keep checking back - and let us know if you have any requests by [opening an issue on the GitHub repo](https://github.com/Redocly/redocly-cli/issues).

To use a rule, add its name to the `graphqlRules` configuration section (or just under the `rules` section if you don't need a clear separation), and declare the severity level (either `error`, `warn`, or `off`):

```yaml
rules:
struct: error
graphqlRules:
type-description: warn
```

## Configurable rule example

Redocly CLI also offers [configurable rules](../rules/configurable-rules.md) that enable you to set assertions about the document being linted.
This functionality works for GraphQL too.
Instead of OpenAPI node types, the `subject.type` targets GraphQL AST node kinds, such as `ObjectTypeDefinition`, `FieldDefinition`, `EnumTypeDefinition`, or `ScalarTypeDefinition`.

The following example shows a configurable rule that displays a warning when an object type name is not in `PascalCase`:

```yaml
rules:
rule/graphql-type-casing:
subject:
type: ObjectTypeDefinition
assertions:
casing: PascalCase
severity: warn
```

With the extensive configurable rules options available, there are many opportunities to make sure that your GraphQL schema conforms with expectations.
We'd also love to see what you're building - it helps us know how things are going!
1 change: 1 addition & 0 deletions docs/@v2/v2.sidebars.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
page: guides/response-contains-property.md
- page: guides/lint-asyncapi.md
- page: guides/lint-arazzo.md
- page: guides/lint-graphql.md
- label: Change the OAuth2 token URL
page: guides/change-token-url.md
- label: Hide OpenAPI specification extensions
Expand Down
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"cookie": "^0.7.2",
"dotenv": "16.4.7",
"glob": "^13.0.5",
"graphql": "^16.14.1",
"handlebars": "^4.7.9",
"https-proxy-agent": "^7.0.5",
"mobx": "^6.0.4",
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/__tests__/fixtures/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const configFixture: Config = {
arazzo1: {},
overlay1: {},
openrpc1: {},
graphql: {},
},
preprocessors: {
oas2: {},
Expand All @@ -38,6 +39,7 @@ export const configFixture: Config = {
arazzo1: {},
overlay1: {},
openrpc1: {},
graphql: {},
},
plugins: [],
doNotResolveExamples: false,
Expand All @@ -51,6 +53,7 @@ export const configFixture: Config = {
arazzo1: {},
overlay1: {},
openrpc1: {},
graphql: {},
},
resolveIgnore: vi.fn(),
addProblemToIgnore: vi.fn(),
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,7 @@ describe('checkIfRulesetExist', () => {
arazzo1: {},
overlay1: {},
openrpc1: {},
graphql: {},
};
expect(() => checkIfRulesetExist(rules)).toThrowError(
'⚠️ No rules were configured. Learn how to configure rules: https://redocly.com/docs/cli/rules/'
Expand All @@ -631,6 +632,7 @@ describe('checkIfRulesetExist', () => {
arazzo1: {},
overlay1: {},
openrpc1: {},
graphql: {},
};
checkIfRulesetExist(rules);
});
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/utils/miscellaneous.ts
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,8 @@ export function checkIfRulesetExist(rules: typeof Config.prototype.rules) {
...rules.async3,
...rules.arazzo1,
...rules.overlay1,
...rules.openrpc1,
...rules.graphql,
};

if (isEmptyObject(ruleset)) {
Expand Down
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"ajv": "npm:@redocly/ajv@8.18.1",
"ajv-formats": "^3.0.1",
"colorette": "^1.2.0",
"graphql": "^16.14.1",
"js-levenshtein": "^1.1.6",
"js-yaml": "^4.1.0",
"picomatch": "^4.0.4",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ exports[`createConfigTypes > matches snapshot for the default config schema 1`]
"properties": {},
},
"graphql": "rootRedoclyConfigSchema.apis_additionalProperties.graphql",
"graphqlRules": "Rules",
"metadata": {
"type": "object",
},
Expand Down Expand Up @@ -301,6 +302,7 @@ exports[`createConfigTypes > matches snapshot for the default config schema 1`]
"name": "Extends",
"properties": {},
},
"graphqlRules": "Rules",
"oas2Decorators": "Decorators",
"oas2Preprocessors": "Preprocessors",
"oas2Rules": "Rules",
Expand Down Expand Up @@ -384,6 +386,7 @@ exports[`createConfigTypes > matches snapshot for the default config schema 1`]
"feedback": "rootRedoclyConfigSchema.feedback",
"footer": "rootRedoclyConfigSchema.footer",
"graphql": "rootRedoclyConfigSchema.graphql",
"graphqlRules": "Rules",
"i18n": "rootRedoclyConfigSchema.i18n",
"ignore": {
"items": {
Expand Down Expand Up @@ -942,6 +945,7 @@ exports[`createConfigTypes > matches snapshot for the default config schema 1`]
"name": "Extends",
"properties": {},
},
"graphqlRules": "Rules",
"name": {
"type": "string",
},
Expand Down Expand Up @@ -1156,6 +1160,7 @@ exports[`createConfigTypes > matches snapshot for the default config schema 1`]
"property": [Function],
"type": {
"description": "REQUIRED. Locates the OpenAPI node type that the lint command evaluates.",
"documentationLink": "https://redocly.com/docs/cli/rules/configurable-rules#subject-object",
"enum": [
"any",
"Root",
Expand Down Expand Up @@ -1359,6 +1364,55 @@ exports[`createConfigTypes > matches snapshot for the default config schema 1`]
"NamedContentDescriptors",
"NamedErrors",
"NamedExamplePairingObjects",
"Name",
"Document",
"OperationDefinition",
"VariableDefinition",
"SelectionSet",
"Field",
"Argument",
"FragmentSpread",
"InlineFragment",
"FragmentDefinition",
"Variable",
"IntValue",
"FloatValue",
"StringValue",
"BooleanValue",
"NullValue",
"EnumValue",
"ListValue",
"ObjectValue",
"ObjectField",
"Directive",
"NamedType",
"ListType",
"NonNullType",
"SchemaDefinition",
"OperationTypeDefinition",
"ScalarTypeDefinition",
"ObjectTypeDefinition",
"FieldDefinition",
"InputValueDefinition",
"InterfaceTypeDefinition",
"UnionTypeDefinition",
"EnumTypeDefinition",
"EnumValueDefinition",
"InputObjectTypeDefinition",
"DirectiveDefinition",
"SchemaExtension",
"DirectiveExtension",
"ScalarTypeExtension",
"ObjectTypeExtension",
"InterfaceTypeExtension",
"UnionTypeExtension",
"EnumTypeExtension",
"InputObjectTypeExtension",
"TypeCoordinate",
"MemberCoordinate",
"ArgumentCoordinate",
"DirectiveCoordinate",
"DirectiveArgumentCoordinate",
"SpecExtension",
],
},
Expand Down
35 changes: 35 additions & 0 deletions packages/core/src/__tests__/lint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,41 @@ describe('lint', () => {
`);
});

it('lintConfig should suggest the correct GraphQL node kind for a misspelled configurable rule subject.type', async () => {
const testConfigContent = outdent`
rules:
rule/graphql-type-casing:
subject:
type: ObjectTypeDefinitio
property: name
assertions:
casing: PascalCase
`;
const config = await createConfig(testConfigContent);
const results = await lintConfig({ config });

expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
[
{
"from": undefined,
"location": [
{
"pointer": "#/rules/rule~1graphql-type-casing/subject/type",
"reportOnKey": false,
"source": "",
},
],
"message": "\`type\` "ObjectTypeDefinitio" is not a valid value. See the supported values: https://redocly.com/docs/cli/rules/configurable-rules#subject-object.",
"ruleId": "configuration struct",
"severity": "error",
"suggest": [
"ObjectTypeDefinition",
],
},
]
`);
});

it("'plugins' shouldn't be allowed in 'apis'", async () => {
const testConfigContent = outdent`
apis:
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/bundle/bundle-visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ export function mapTypeToComponent(typeName: string, version: SpecMajorVersion)
default:
return null;
}
case 'graphql':
// GraphQL SDL is never bundled/$ref-resolved.
return null;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ exports[`resolveConfig > should ignore minimal from the root and read local file
"tags-alphabetical": "off",
},
"decorators": {},
"graphqlRules": {
"no-unused-types": "warn",
"type-description": "off",
},
"oas2Decorators": {},
"oas2Preprocessors": {},
"oas2Rules": {
Expand Down Expand Up @@ -448,6 +452,10 @@ exports[`resolveConfig > should resolve extends with local file config which con
"tags-alphabetical": "off",
},
"decorators": {},
"graphqlRules": {
"no-unused-types": "warn",
"type-description": "off",
},
"oas2Decorators": {},
"oas2Preprocessors": {},
"oas2Rules": {
Expand Down
Loading
Loading