Skip to content

Commit

Permalink
feat(rule): Add rule to check that id is only used for Relay IDs
Browse files Browse the repository at this point in the history
In Relay, `id` is [treated as a reserved word and must be a globally unique value](facebook/relay#1682 (comment)).
That means when an `id` field is present on a type, it must be the type that Relay expects (a string type).

This linter assumes that an `ID` scalar is present in the schema, and checks that if an `id` field is present, it is using that type.
  • Loading branch information
steverice committed Mar 7, 2023
1 parent b0f7186 commit 2f56901
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 0 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@ Also, it adds some validation regarding Pinterest-specific rules:

- Only edge type names may end in `Edge`. These object types are considered edge types.

### `relay-id-field-type`

This rule validates that when an `id` field is used, it is of an `ID` scalar type that is assumed to exist in the schema as a string scalar.

Relay [imposes this requirement](https://github.com/facebook/relay/issues/1682#issuecomment-296393416) because it makes assumptions about any `id` field that it sees.

## Contributing

See [`CONTRIBUTING.md`](CONTRIBUTING.md).
35 changes: 35 additions & 0 deletions src/rules/relayIdFieldType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type {
ASTVisitor,
FieldDefinitionNode,
ValidationContext,
} from 'graphql';

import { ValidationError } from 'graphql-schema-linter';
import { isNamedTypeNode, isNonNullTypeNode } from '../utils';

function isProperRelayIdField(field: FieldDefinitionNode): boolean {
return (
isNonNullTypeNode(field.type) &&
isNamedTypeNode(field.type.type) &&
field.type.type.name.value == 'ID'
);
}

export function RelayIdFieldType(context: ValidationContext): ASTVisitor {
return {
ObjectTypeDefinition(node) {
const idField = node.fields?.find(
(field: FieldDefinitionNode) => field.name.value === 'id',
);
if (idField && !isProperRelayIdField(idField)) {
context.reportError(
new ValidationError(
'relay-id-field-type',
`The "id" field on \`${node.name.value}\` must be a proper Relay ID field type, or renamed.`,
[node],
),
);
}
},
};
}
76 changes: 76 additions & 0 deletions test/rules/testRelayIdFieldType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { RelayIdFieldType } from '../../src/rules/relayIdFieldType';

import { gql } from '../utils';

import { expectPassesRule, expectFailsRule } from '../assertions';

describe('RelayIdFieldType rule', () => {
it('allows types that have no id field', () => {
expectPassesRule(
RelayIdFieldType,
gql`
type Entity {
entityId: String!
}
`,
);
});

it('allows types with a correct id field', () => {
expectPassesRule(
RelayIdFieldType,
gql`
type Entity {
id: ID!
}
`,
);
});

it('allows types to use ID on other fields', () => {
expectPassesRule(
RelayIdFieldType,
gql`
type Entity {
myId: ID!
}
`,
);
});

it('disallows types with a wrong id field type', () => {
expectFailsRule(
RelayIdFieldType,
gql`
type Entity {
id: String!
}
`,
[
{
message:
'The "id" field on `Entity` must be a proper Relay ID field type, or renamed.',
locations: [{ line: 2, column: 9 }],
},
],
);
});

it('disallows types with a non-nullable id field type', () => {
expectFailsRule(
RelayIdFieldType,
gql`
type Entity {
id: ID
}
`,
[
{
message:
'The "id" field on `Entity` must be a proper Relay ID field type, or renamed.',
locations: [{ line: 2, column: 9 }],
},
],
);
});
});

0 comments on commit 2f56901

Please sign in to comment.