graphql#ObjectTypeDefinitionNode TypeScript Examples

The following examples show how to use graphql#ObjectTypeDefinitionNode. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: appsync-visitor.ts    From amplify-codegen with Apache License 2.0 6 votes vote down vote up
ObjectTypeDefinition(node: ObjectTypeDefinitionNode, index?: string | number, parent?: any) {
    if (this.typesToSkip.includes(node.name.value)) {
      // Skip Query, mutation and subscription type
      return;
    }
    const directives = this.getDirectives(node.directives);
    const fields = (node.fields as unknown) as CodeGenField[];
    const modelDirective = directives.find(directive => directive.name === 'model');
    if (modelDirective) {
      // Todo: Add validation for each directives
      // @model would add the id: ID! if missing or throw error if there is an id of different type
      // @key check if fields listed in directives are present in the Object
      //
      const model: CodeGenModel = {
        name: node.name.value,
        type: 'model',
        directives,
        fields,
      };
      this.ensureIdField(model);
      this.addTimestampFields(model, modelDirective);
      this.sortFields(model);
      this.modelMap[node.name.value] = model;
    } else {
      const nonModel: CodeGenModel = {
        name: node.name.value,
        type: 'model',
        directives,
        fields,
      };
      this.nonModelMap[node.name.value] = nonModel;
    }
  }
Example #2
Source File: ttl-transformer.ts    From graphql-ttl-transformer with MIT License 6 votes vote down vote up
public field = (
    parent: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode,
    definition: FieldDefinitionNode,
    directive: DirectiveNode,
    acc: TransformerContext
  ) => {
    if (!["AWSTimestamp", "Int"].includes(getBaseType(definition.type))) {
      throw new InvalidDirectiveError(
        'Directive "ttl" must be used only on AWSTimestamp or Int type fields.'
      );
    }

    let numberOfTtlDirectivesInsideParentType = 0;
    if (parent.fields) {
      parent.fields.forEach((field) => {
        if (field.directives) {
          numberOfTtlDirectivesInsideParentType += field.directives.filter(
            (directive) => directive.name.value === "ttl"
          ).length;
        }
      });
    }
    if (numberOfTtlDirectivesInsideParentType > 1) {
      throw new InvalidDirectiveError(
        'Directive "ttl" must be used only once in the same type.'
      );
    }

    const tableName = ModelResourceIDs.ModelTableResourceID(parent.name.value);
    const table = acc.getResource(tableName);
    const fieldName = definition.name.value;
    table.Properties = {
      ...table.Properties,
      TimeToLiveSpecification: {
        AttributeName: fieldName,
        Enabled: true,
      },
    };
  };
Example #3
Source File: no-typename-prefix.ts    From graphql-eslint with MIT License 5 votes vote down vote up
rule: GraphQLESLintRule = {
  meta: {
    type: 'suggestion',
    hasSuggestions: true,
    docs: {
      category: 'Schema',
      description: 'Enforces users to avoid using the type name in a field name while defining your schema.',
      recommended: true,
      url: 'https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/no-typename-prefix.md',
      examples: [
        {
          title: 'Incorrect',
          code: /* GraphQL */ `
            type User {
              userId: ID!
            }
          `,
        },
        {
          title: 'Correct',
          code: /* GraphQL */ `
            type User {
              id: ID!
            }
          `,
        },
      ],
    },
    messages: {
      [NO_TYPENAME_PREFIX]: 'Field "{{ fieldName }}" starts with the name of the parent type "{{ typeName }}"',
    },
    schema: [],
  },
  create(context) {
    return {
      'ObjectTypeDefinition, ObjectTypeExtension, InterfaceTypeDefinition, InterfaceTypeExtension'(
        node: GraphQLESTreeNode<
          ObjectTypeDefinitionNode | ObjectTypeExtensionNode | InterfaceTypeDefinitionNode | InterfaceTypeExtensionNode
        >
      ) {
        const typeName = node.name.value;
        const lowerTypeName = typeName.toLowerCase();

        for (const field of node.fields) {
          const fieldName = field.name.value;

          if (fieldName.toLowerCase().startsWith(lowerTypeName)) {
            context.report({
              data: {
                fieldName,
                typeName,
              },
              messageId: NO_TYPENAME_PREFIX,
              node: field.name,
              suggest: [
                {
                  desc: `Remove \`${fieldName.slice(0, typeName.length)}\` prefix`,
                  fix: fixer =>
                    fixer.replaceText(field.name as any, fieldName.replace(new RegExp(`^${typeName}`, 'i'), '')),
                },
              ],
            });
          }
        }
      },
    };
  },
}
Example #4
Source File: relay-connection-types.ts    From graphql-eslint with MIT License 5 votes vote down vote up
hasEdgesField = (node: GraphQLESTreeNode<ObjectTypeDefinitionNode>) =>
  node.fields.some(field => field.name.value === 'edges')
Example #5
Source File: relay-connection-types.ts    From graphql-eslint with MIT License 5 votes vote down vote up
hasPageInfoField = (node: GraphQLESTreeNode<ObjectTypeDefinitionNode>) =>
  node.fields.some(field => field.name.value === 'pageInfo')
Example #6
Source File: alphabetize.ts    From graphql-eslint with MIT License 4 votes vote down vote up
rule: GraphQLESLintRule<[AlphabetizeConfig]> = {
  meta: {
    type: 'suggestion',
    fixable: 'code',
    docs: {
      category: ['Schema', 'Operations'],
      description:
        'Enforce arrange in alphabetical order for type fields, enum values, input object fields, operation selections and more.',
      url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
      examples: [
        {
          title: 'Incorrect',
          usage: [{ fields: [Kind.OBJECT_TYPE_DEFINITION] }],
          code: /* GraphQL */ `
            type User {
              password: String
              firstName: String! # should be before "password"
              age: Int # should be before "firstName"
              lastName: String!
            }
          `,
        },
        {
          title: 'Correct',
          usage: [{ fields: [Kind.OBJECT_TYPE_DEFINITION] }],
          code: /* GraphQL */ `
            type User {
              age: Int
              firstName: String!
              lastName: String!
              password: String
            }
          `,
        },
        {
          title: 'Incorrect',
          usage: [{ values: [Kind.ENUM_TYPE_DEFINITION] }],
          code: /* GraphQL */ `
            enum Role {
              SUPER_ADMIN
              ADMIN # should be before "SUPER_ADMIN"
              USER
              GOD # should be before "USER"
            }
          `,
        },
        {
          title: 'Correct',
          usage: [{ values: [Kind.ENUM_TYPE_DEFINITION] }],
          code: /* GraphQL */ `
            enum Role {
              ADMIN
              GOD
              SUPER_ADMIN
              USER
            }
          `,
        },
        {
          title: 'Incorrect',
          usage: [{ selections: [Kind.OPERATION_DEFINITION] }],
          code: /* GraphQL */ `
            query {
              me {
                firstName
                lastName
                email # should be before "lastName"
              }
            }
          `,
        },
        {
          title: 'Correct',
          usage: [{ selections: [Kind.OPERATION_DEFINITION] }],
          code: /* GraphQL */ `
            query {
              me {
                email
                firstName
                lastName
              }
            }
          `,
        },
      ],
      configOptions: {
        schema: [
          {
            fields: fieldsEnum,
            values: valuesEnum,
            arguments: argumentsEnum,
            // TODO: add in graphql-eslint v4
            // definitions: true,
          },
        ],
        operations: [
          {
            selections: selectionsEnum,
            variables: variablesEnum,
            arguments: [Kind.FIELD, Kind.DIRECTIVE],
          },
        ],
      },
    },
    messages: {
      [RULE_ID]: '`{{ currName }}` should be before {{ prevName }}.',
    },
    schema: {
      type: 'array',
      minItems: 1,
      maxItems: 1,
      items: {
        type: 'object',
        additionalProperties: false,
        minProperties: 1,
        properties: {
          fields: {
            ...ARRAY_DEFAULT_OPTIONS,
            items: {
              enum: fieldsEnum,
            },
            description: 'Fields of `type`, `interface`, and `input`.',
          },
          values: {
            ...ARRAY_DEFAULT_OPTIONS,
            items: {
              enum: valuesEnum,
            },
            description: 'Values of `enum`.',
          },
          selections: {
            ...ARRAY_DEFAULT_OPTIONS,
            items: {
              enum: selectionsEnum,
            },
            description: 'Selections of `fragment` and operations `query`, `mutation` and `subscription`.',
          },
          variables: {
            ...ARRAY_DEFAULT_OPTIONS,
            items: {
              enum: variablesEnum,
            },
            description: 'Variables of operations `query`, `mutation` and `subscription`.',
          },
          arguments: {
            ...ARRAY_DEFAULT_OPTIONS,
            items: {
              enum: argumentsEnum,
            },
            description: 'Arguments of fields and directives.',
          },
          definitions: {
            type: 'boolean',
            description: 'Definitions – `type`, `interface`, `enum`, `scalar`, `input`, `union` and `directive`.',
            default: false,
          },
        },
      },
    },
  },
  create(context) {
    const sourceCode = context.getSourceCode();

    function isNodeAndCommentOnSameLine(node: { loc: SourceLocation }, comment: Comment): boolean {
      return node.loc.end.line === comment.loc.start.line;
    }

    function getBeforeComments(node): Comment[] {
      const commentsBefore = sourceCode.getCommentsBefore(node);
      if (commentsBefore.length === 0) {
        return [];
      }
      const tokenBefore = sourceCode.getTokenBefore(node);
      if (tokenBefore) {
        return commentsBefore.filter(comment => !isNodeAndCommentOnSameLine(tokenBefore, comment));
      }
      const filteredComments = [];
      const nodeLine = node.loc.start.line;
      // Break on comment that not attached to node
      for (let i = commentsBefore.length - 1; i >= 0; i -= 1) {
        const comment = commentsBefore[i];
        if (nodeLine - comment.loc.start.line - filteredComments.length > 1) {
          break;
        }
        filteredComments.unshift(comment);
      }
      return filteredComments;
    }

    function getRangeWithComments(node): AST.Range {
      if (node.kind === Kind.VARIABLE) {
        node = node.parent;
      }
      const [firstBeforeComment] = getBeforeComments(node);
      const [firstAfterComment] = sourceCode.getCommentsAfter(node);
      const from = firstBeforeComment || node;
      const to = firstAfterComment && isNodeAndCommentOnSameLine(node, firstAfterComment) ? firstAfterComment : node;
      return [from.range[0], to.range[1]];
    }

    function checkNodes(nodes: GraphQLESTreeNode<ASTNode>[]) {
      // Starts from 1, ignore nodes.length <= 1
      for (let i = 1; i < nodes.length; i += 1) {
        const currNode = nodes[i];
        const currName = 'name' in currNode && currNode.name?.value;
        if (!currName) {
          // we don't move unnamed current nodes
          continue;
        }

        const prevNode = nodes[i - 1];
        const prevName = 'name' in prevNode && prevNode.name?.value;
        if (prevName) {
          // Compare with lexicographic order
          const compareResult = prevName.localeCompare(currName);
          const shouldSort = compareResult === 1;
          if (!shouldSort) {
            const isSameName = compareResult === 0;
            if (!isSameName || !prevNode.kind.endsWith('Extension') || currNode.kind.endsWith('Extension')) {
              continue;
            }
          }
        }

        context.report({
          node: currNode.name,
          messageId: RULE_ID,
          data: {
            currName,
            prevName: prevName ? `\`${prevName}\`` : lowerCase(prevNode.kind),
          },
          *fix(fixer) {
            const prevRange = getRangeWithComments(prevNode);
            const currRange = getRangeWithComments(currNode);
            yield fixer.replaceTextRange(prevRange, sourceCode.getText({ range: currRange } as any));
            yield fixer.replaceTextRange(currRange, sourceCode.getText({ range: prevRange } as any));
          },
        });
      }
    }

    const opts = context.options[0];
    const fields = new Set(opts.fields ?? []);
    const listeners: GraphQLESLintRuleListener = {};

    const kinds = [
      fields.has(Kind.OBJECT_TYPE_DEFINITION) && [Kind.OBJECT_TYPE_DEFINITION, Kind.OBJECT_TYPE_EXTENSION],
      fields.has(Kind.INTERFACE_TYPE_DEFINITION) && [Kind.INTERFACE_TYPE_DEFINITION, Kind.INTERFACE_TYPE_EXTENSION],
      fields.has(Kind.INPUT_OBJECT_TYPE_DEFINITION) && [
        Kind.INPUT_OBJECT_TYPE_DEFINITION,
        Kind.INPUT_OBJECT_TYPE_EXTENSION,
      ],
    ]
      .filter(Boolean)
      .flat();

    const fieldsSelector = kinds.join(',');

    const hasEnumValues = opts.values?.[0] === Kind.ENUM_TYPE_DEFINITION;
    const selectionsSelector = opts.selections?.join(',');
    const hasVariables = opts.variables?.[0] === Kind.OPERATION_DEFINITION;
    const argumentsSelector = opts.arguments?.join(',');

    if (fieldsSelector) {
      listeners[fieldsSelector] = (
        node: GraphQLESTreeNode<
          | ObjectTypeDefinitionNode
          | ObjectTypeExtensionNode
          | InterfaceTypeDefinitionNode
          | InterfaceTypeExtensionNode
          | InputObjectTypeDefinitionNode
          | InputObjectTypeExtensionNode
        >
      ) => {
        checkNodes(node.fields);
      };
    }

    if (hasEnumValues) {
      const enumValuesSelector = [Kind.ENUM_TYPE_DEFINITION, Kind.ENUM_TYPE_EXTENSION].join(',');
      listeners[enumValuesSelector] = (node: GraphQLESTreeNode<EnumTypeDefinitionNode | EnumTypeExtensionNode>) => {
        checkNodes(node.values);
      };
    }

    if (selectionsSelector) {
      listeners[`:matches(${selectionsSelector}) SelectionSet`] = (node: GraphQLESTreeNode<SelectionSetNode>) => {
        checkNodes(
          node.selections.map(selection =>
            // sort by alias is field is renamed
            'alias' in selection && selection.alias ? ({ name: selection.alias } as any) : selection
          )
        );
      };
    }

    if (hasVariables) {
      listeners.OperationDefinition = (node: GraphQLESTreeNode<OperationDefinitionNode>) => {
        checkNodes(node.variableDefinitions.map(varDef => varDef.variable));
      };
    }

    if (argumentsSelector) {
      listeners[argumentsSelector] = (
        node: GraphQLESTreeNode<FieldDefinitionNode | FieldNode | DirectiveDefinitionNode | DirectiveNode>
      ) => {
        checkNodes(node.arguments);
      };
    }

    if (opts.definitions) {
      listeners.Document = node => {
        checkNodes(node.definitions);
      };
    }

    return listeners;
  },
}
Example #7
Source File: relay-connection-types.ts    From graphql-eslint with MIT License 4 votes vote down vote up
rule: GraphQLESLintRule = {
  meta: {
    type: 'problem',
    docs: {
      category: 'Schema',
      description: [
        'Set of rules to follow Relay specification for Connection types.',
        '',
        '- Any type whose name ends in "Connection" is considered by spec to be a `Connection type`',
        '- Connection type must be an Object type',
        '- Connection type must contain a field `edges` that return a list type that wraps an edge type',
        '- Connection type must contain a field `pageInfo` that return a non-null `PageInfo` Object type',
      ].join('\n'),
      url: 'https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/relay-connection-types.md',
      isDisabledForAllConfig: true,
      examples: [
        {
          title: 'Incorrect',
          code: /* GraphQL */ `
            type UserPayload { # should be an Object type with \`Connection\` suffix
              edges: UserEdge! # should return a list type
              pageInfo: PageInfo # should return a non-null \`PageInfo\` Object type
            }
          `,
        },
        {
          title: 'Correct',
          code: /* GraphQL */ `
            type UserConnection {
              edges: [UserEdge]
              pageInfo: PageInfo!
            }
          `,
        },
      ],
    },
    messages: {
      // Connection types
      [MUST_BE_OBJECT_TYPE]: 'Connection type must be an Object type.',
      [MUST_HAVE_CONNECTION_SUFFIX]: 'Connection type must have `Connection` suffix.',
      [MUST_CONTAIN_FIELD_EDGES]: 'Connection type must contain a field `edges` that return a list type.',
      [MUST_CONTAIN_FIELD_PAGE_INFO]:
        'Connection type must contain a field `pageInfo` that return a non-null `PageInfo` Object type.',
      [EDGES_FIELD_MUST_RETURN_LIST_TYPE]: '`edges` field must return a list type.',
      [PAGE_INFO_FIELD_MUST_RETURN_NON_NULL_TYPE]: '`pageInfo` field must return a non-null `PageInfo` Object type.',
    },
    schema: [],
  },
  create(context) {
    return {
      [notConnectionTypesSelector](node) {
        context.report({ node, messageId: MUST_BE_OBJECT_TYPE });
      },
      ':matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value!=/Connection$/]'(
        node: GraphQLESTreeNode<ObjectTypeDefinitionNode>
      ) {
        if (hasEdgesField(node) && hasPageInfoField(node)) {
          context.report({ node: node.name, messageId: MUST_HAVE_CONNECTION_SUFFIX });
        }
      },
      ':matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=/Connection$/]'(
        node: GraphQLESTreeNode<ObjectTypeDefinitionNode>
      ) {
        if (!hasEdgesField(node)) {
          context.report({ node: node.name, messageId: MUST_CONTAIN_FIELD_EDGES });
        }
        if (!hasPageInfoField(node)) {
          context.report({ node: node.name, messageId: MUST_CONTAIN_FIELD_PAGE_INFO });
        }
      },
      ':matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=/Connection$/] > FieldDefinition[name.value=edges] > .gqlType'(
        node: GraphQLESTreeNode<TypeNode>
      ) {
        const isListType =
          node.kind === Kind.LIST_TYPE || (node.kind === Kind.NON_NULL_TYPE && node.gqlType.kind === Kind.LIST_TYPE);
        if (!isListType) {
          context.report({ node, messageId: EDGES_FIELD_MUST_RETURN_LIST_TYPE });
        }
      },
      ':matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=/Connection$/] > FieldDefinition[name.value=pageInfo] > .gqlType'(
        node: GraphQLESTreeNode<TypeNode>
      ) {
        const isNonNullPageInfoType =
          node.kind === Kind.NON_NULL_TYPE &&
          node.gqlType.kind === Kind.NAMED_TYPE &&
          node.gqlType.name.value === 'PageInfo';
        if (!isNonNullPageInfoType) {
          context.report({ node, messageId: PAGE_INFO_FIELD_MUST_RETURN_NON_NULL_TYPE });
        }
      },
    };
  },
}
Example #8
Source File: relay-edge-types.ts    From graphql-eslint with MIT License 4 votes vote down vote up
rule: GraphQLESLintRule<[EdgeTypesConfig], true> = {
  meta: {
    type: 'problem',
    docs: {
      category: 'Schema',
      description: [
        'Set of rules to follow Relay specification for Edge types.',
        '',
        "- A type that is returned in list form by a connection type's `edges` field is considered by this spec to be an Edge type",
        '- Edge type must be an Object type',
        '- Edge type must contain a field `node` that return either Scalar, Enum, Object, Interface, Union, or a non-null wrapper around one of those types. Notably, this field cannot return a list',
        '- Edge type must contain a field `cursor` that return either String, Scalar, or a non-null wrapper around one of those types',
        '- Edge type name must end in "Edge" _(optional)_',
        "- Edge type's field `node` must implement `Node` interface _(optional)_",
        '- A list type should only wrap an edge type _(optional)_',
      ].join('\n'),
      url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
      isDisabledForAllConfig: true,
      requiresSchema: true,
      examples: [
        {
          title: 'Correct',
          code: /* GraphQL */ `
            type UserConnection {
              edges: [UserEdge]
              pageInfo: PageInfo!
            }
          `,
        },
      ],
    },
    messages: {
      [MESSAGE_MUST_BE_OBJECT_TYPE]: 'Edge type must be an Object type.',
      [MESSAGE_MISSING_EDGE_SUFFIX]: 'Edge type must have "Edge" suffix.',
      [MESSAGE_LIST_TYPE_ONLY_EDGE_TYPE]: 'A list type should only wrap an edge type.',
      [MESSAGE_SHOULD_IMPLEMENTS_NODE]: "Edge type's field `node` must implement `Node` interface.",
    },
    schema: {
      type: 'array',
      maxItems: 1,
      items: {
        type: 'object',
        additionalProperties: false,
        minProperties: 1,
        properties: {
          withEdgeSuffix: {
            type: 'boolean',
            default: true,
            description: 'Edge type name must end in "Edge".',
          },
          shouldImplementNode: {
            type: 'boolean',
            default: true,
            description: "Edge type's field `node` must implement `Node` interface.",
          },
          listTypeCanWrapOnlyEdgeType: {
            type: 'boolean',
            default: true,
            description: 'A list type should only wrap an edge type.',
          },
        },
      },
    },
  },
  create(context) {
    const schema = requireGraphQLSchemaFromContext(RULE_ID, context);
    const edgeTypes = getEdgeTypes(schema);
    const options: EdgeTypesConfig = {
      withEdgeSuffix: true,
      shouldImplementNode: true,
      listTypeCanWrapOnlyEdgeType: true,
      ...context.options[0],
    };

    const isNamedOrNonNullNamed = (node: GraphQLESTreeNode<TypeNode>): boolean =>
      node.kind === Kind.NAMED_TYPE || (node.kind === Kind.NON_NULL_TYPE && node.gqlType.kind === Kind.NAMED_TYPE);

    const checkNodeField = (node: GraphQLESTreeNode<ObjectTypeDefinitionNode>): void => {
      const nodeField = node.fields.find(field => field.name.value === 'node');
      const message =
        'return either a Scalar, Enum, Object, Interface, Union, or a non-null wrapper around one of those types.';
      if (!nodeField) {
        context.report({
          node: node.name,
          message: `Edge type must contain a field \`node\` that ${message}`,
        });
      } else if (!isNamedOrNonNullNamed(nodeField.gqlType)) {
        context.report({ node: nodeField.name, message: `Field \`node\` must ${message}` });
      } else if (options.shouldImplementNode) {
        const nodeReturnTypeName = getTypeName(nodeField.gqlType.rawNode());
        const type = schema.getType(nodeReturnTypeName);
        if (!isObjectType(type)) {
          return;
        }
        const implementsNode = type.astNode.interfaces.some(n => n.name.value === 'Node');
        if (!implementsNode) {
          context.report({ node: node.name, messageId: MESSAGE_SHOULD_IMPLEMENTS_NODE });
        }
      }
    };

    const checkCursorField = (node: GraphQLESTreeNode<ObjectTypeDefinitionNode>): void => {
      const cursorField = node.fields.find(field => field.name.value === 'cursor');
      const message = 'return either a String, Scalar, or a non-null wrapper wrapper around one of those types.';
      if (!cursorField) {
        context.report({
          node: node.name,
          message: `Edge type must contain a field \`cursor\` that ${message}`,
        });
        return;
      }
      const typeName = getTypeName(cursorField.rawNode());
      if (
        !isNamedOrNonNullNamed(cursorField.gqlType) ||
        (typeName !== 'String' && !isScalarType(schema.getType(typeName)))
      ) {
        context.report({ node: cursorField.name, message: `Field \`cursor\` must ${message}` });
      }
    };

    const listeners: GraphQLESLintRuleListener<true> = {
      ':matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=/Connection$/] > FieldDefinition[name.value=edges] > .gqlType Name'(
        node: GraphQLESTreeNode<NameNode>
      ) {
        const type = schema.getType(node.value);
        if (!isObjectType(type)) {
          context.report({ node, messageId: MESSAGE_MUST_BE_OBJECT_TYPE });
        }
      },
      ':matches(ObjectTypeDefinition, ObjectTypeExtension)'(node: GraphQLESTreeNode<ObjectTypeDefinitionNode>) {
        const typeName = node.name.value;
        if (edgeTypes.has(typeName)) {
          checkNodeField(node);
          checkCursorField(node);
          if (options.withEdgeSuffix && !typeName.endsWith('Edge')) {
            context.report({ node: node.name, messageId: MESSAGE_MISSING_EDGE_SUFFIX });
          }
        }
      },
    };

    if (options.listTypeCanWrapOnlyEdgeType) {
      listeners['FieldDefinition > .gqlType'] = (node: GraphQLESTreeNode<TypeNode>) => {
        if (
          node.kind === Kind.LIST_TYPE ||
          (node.kind === Kind.NON_NULL_TYPE && node.gqlType.kind === Kind.LIST_TYPE)
        ) {
          const typeName = getTypeName(node.rawNode());
          if (!edgeTypes.has(typeName)) {
            context.report({ node, messageId: MESSAGE_LIST_TYPE_ONLY_EDGE_TYPE });
          }
        }
      };
    }

    return listeners;
  },
}
Example #9
Source File: relay-page-info.ts    From graphql-eslint with MIT License 4 votes vote down vote up
rule: GraphQLESLintRule = {
  meta: {
    type: 'problem',
    docs: {
      category: 'Schema',
      description: [
        'Set of rules to follow Relay specification for `PageInfo` object.',
        '',
        '- `PageInfo` must be an Object type',
        '- `PageInfo` must contain fields `hasPreviousPage` and `hasNextPage`, that return non-null Boolean',
        '- `PageInfo` must contain fields `startCursor` and `endCursor`, that return either String or Scalar, which can be null if there are no results',
      ].join('\n'),
      url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
      examples: [
        {
          title: 'Correct',
          code: /* GraphQL */ `
            type PageInfo {
              hasPreviousPage: Boolean!
              hasNextPage: Boolean!
              startCursor: String
              endCursor: String
            }
          `,
        },
      ],
      isDisabledForAllConfig: true,
      requiresSchema: true,
    },
    messages: {
      [MESSAGE_MUST_EXIST]: 'The server must provide a `PageInfo` object.',
      [MESSAGE_MUST_BE_OBJECT_TYPE]: '`PageInfo` must be an Object type.',
    },
    schema: [],
  },
  create(context) {
    const schema = requireGraphQLSchemaFromContext(RULE_ID, context);
    if (process.env.NODE_ENV === 'test' || !hasPageInfoChecked) {
      const pageInfoType = schema.getType('PageInfo');
      if (!pageInfoType) {
        context.report({
          loc: REPORT_ON_FIRST_CHARACTER,
          messageId: MESSAGE_MUST_EXIST,
        });
      }
      hasPageInfoChecked = true;
    }
    return {
      [notPageInfoTypesSelector](node) {
        context.report({ node, messageId: MESSAGE_MUST_BE_OBJECT_TYPE });
      },
      'ObjectTypeDefinition[name.value=PageInfo]'(node: GraphQLESTreeNode<ObjectTypeDefinitionNode>) {
        const fieldMap = Object.fromEntries(node.fields.map(field => [field.name.value, field]));

        const checkField = (
          fieldName: 'hasPreviousPage' | 'hasNextPage' | 'startCursor' | 'endCursor',
          typeName: 'Boolean' | 'String'
        ): void => {
          const field = fieldMap[fieldName];
          let isAllowedType = false;

          if (field) {
            const type = field.gqlType;
            if (typeName === 'Boolean') {
              isAllowedType =
                type.kind === Kind.NON_NULL_TYPE &&
                type.gqlType.kind === Kind.NAMED_TYPE &&
                type.gqlType.name.value === 'Boolean';
            } else if (type.kind === Kind.NAMED_TYPE) {
              isAllowedType = type.name.value === 'String' || isScalarType(schema.getType(type.name.value));
            }
          }

          if (!isAllowedType) {
            const returnType =
              typeName === 'Boolean'
                ? 'non-null Boolean'
                : 'either String or Scalar, which can be null if there are no results';
            context.report({
              node: field ? field.name : node.name,
              message: field
                ? `Field \`${fieldName}\` must return ${returnType}.`
                : `\`PageInfo\` must contain a field \`${fieldName}\`, that return ${returnType}.`,
            });
          }
        };
        checkField('hasPreviousPage', 'Boolean');
        checkField('hasNextPage', 'Boolean');
        checkField('startCursor', 'String');
        checkField('endCursor', 'String');
      },
    };
  },
}
Example #10
Source File: strict-id-in-types.ts    From graphql-eslint with MIT License 4 votes vote down vote up
rule: GraphQLESLintRule<[StrictIdInTypesRuleConfig]> = {
  meta: {
    type: 'suggestion',
    docs: {
      description:
        'Requires output types to have one unique identifier unless they do not have a logical one. Exceptions can be used to ignore output types that do not have unique identifiers.',
      category: 'Schema',
      recommended: true,
      url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
      requiresSchema: true,
      examples: [
        {
          title: 'Incorrect',
          usage: [
            {
              acceptedIdNames: ['id', '_id'],
              acceptedIdTypes: ['ID'],
              exceptions: { suffixes: ['Payload'] },
            },
          ],
          code: /* GraphQL */ `
            # Incorrect field name
            type InvalidFieldName {
              key: ID!
            }

            # Incorrect field type
            type InvalidFieldType {
              id: String!
            }

            # Incorrect exception suffix
            type InvalidSuffixResult {
              data: String!
            }

            # Too many unique identifiers. Must only contain one.
            type InvalidFieldName {
              id: ID!
              _id: ID!
            }
          `,
        },
        {
          title: 'Correct',
          usage: [
            {
              acceptedIdNames: ['id', '_id'],
              acceptedIdTypes: ['ID'],
              exceptions: { types: ['Error'], suffixes: ['Payload'] },
            },
          ],
          code: /* GraphQL */ `
            type User {
              id: ID!
            }

            type Post {
              _id: ID!
            }

            type CreateUserPayload {
              data: String!
            }

            type Error {
              message: String!
            }
          `,
        },
      ],
    },
    schema: {
      type: 'array',
      maxItems: 1,
      items: {
        type: 'object',
        additionalProperties: false,
        properties: {
          acceptedIdNames: {
            ...ARRAY_DEFAULT_OPTIONS,
            default: ['id'],
          },
          acceptedIdTypes: {
            ...ARRAY_DEFAULT_OPTIONS,
            default: ['ID'],
          },
          exceptions: {
            type: 'object',
            properties: {
              types: {
                ...ARRAY_DEFAULT_OPTIONS,
                description: 'This is used to exclude types with names that match one of the specified values.',
              },
              suffixes: {
                ...ARRAY_DEFAULT_OPTIONS,
                description:
                  'This is used to exclude types with names with suffixes that match one of the specified values.',
              },
            },
          },
        },
      },
    },
  },
  create(context) {
    const options: StrictIdInTypesRuleConfig = {
      acceptedIdNames: ['id'],
      acceptedIdTypes: ['ID'],
      exceptions: {},
      ...context.options[0],
    };

    const schema = requireGraphQLSchemaFromContext(RULE_ID, context);
    const rootTypeNames = [schema.getQueryType(), schema.getMutationType(), schema.getSubscriptionType()]
      .filter(Boolean)
      .map(type => type.name);
    const selector = `ObjectTypeDefinition[name.value!=/^(${rootTypeNames.join('|')})$/]`;

    return {
      [selector](node: GraphQLESTreeNode<ObjectTypeDefinitionNode>) {
        const typeName = node.name.value;

        const shouldIgnoreNode =
          options.exceptions.types?.includes(typeName) ||
          options.exceptions.suffixes?.some(suffix => typeName.endsWith(suffix));

        if (shouldIgnoreNode) {
          return;
        }

        const validIds = node.fields.filter(field => {
          const fieldNode = field.rawNode();
          const isValidIdName = options.acceptedIdNames.includes(fieldNode.name.value);

          // To be a valid type, it must be non-null and one of the accepted types.
          let isValidIdType = false;
          if (fieldNode.type.kind === Kind.NON_NULL_TYPE && fieldNode.type.type.kind === Kind.NAMED_TYPE) {
            isValidIdType = options.acceptedIdTypes.includes(fieldNode.type.type.name.value);
          }

          return isValidIdName && isValidIdType;
        });

        // Usually, there should be only one unique identifier field per type.
        // Some clients allow multiple fields to be used. If more people need this,
        // we can extend this rule later.
        if (validIds.length !== 1) {
          const pluralNamesSuffix = options.acceptedIdNames.length > 1 ? 's' : '';
          const pluralTypesSuffix = options.acceptedIdTypes.length > 1 ? 's' : '';
          context.report({
            node: node.name,
            message: `${typeName} must have exactly one non-nullable unique identifier. Accepted name${pluralNamesSuffix}: ${englishJoinWords(
              options.acceptedIdNames
            )}. Accepted type${pluralTypesSuffix}: ${englishJoinWords(options.acceptedIdTypes)}.`,
          });
        }
      },
    };
  },
}