Skip to content

Commit

Permalink
Merge pull request #13 from salesforce/typeUpdate
Browse files Browse the repository at this point in the history
Type update
  • Loading branch information
haifeng-li-at-salesforce committed Jun 4, 2024
2 parents ea70350 + 1cf4c05 commit bf2fd89
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 84 deletions.
30 changes: 15 additions & 15 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
name: lint
run-name: Installs project and runs linting
on: [ push, pull_request ]
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
strategy:
matrix:
node: [ 18, 20 ]
name: Linting on Ubuntu with Node ${{ matrix.node }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
cache: 'npm'
- run: npm install
- run: npm run lint
lint:
runs-on: ubuntu-latest
strategy:
matrix:
node: [18, 20]
name: Linting on Ubuntu with Node ${{ matrix.node }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
cache: 'npm'
- run: npm install
- run: npm run lint
23 changes: 11 additions & 12 deletions src/rules/graphql/no-fiscal-date-filtering-supported.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import { Kind } from 'graphql';
import { ASTNode, Kind, ArgumentNode } from 'graphql';
import { GraphQLESLintRule, GraphQLESLintRuleContext } from '@graphql-eslint/eslint-plugin';
import getDocUrl from '../../util/getDocUrl';
import { getClosestAncestorByType } from '../../util/graphqlAstUtils';
import { GraphQLESTreeNode } from './types';
export const NO_FISCAL_DATE_FILTER_SUPPORTED_RULE_ID =
'offline-graphql-no-fiscal-date-filter-supported';

type NodeWithParent = {
kind: string;
parent: NodeWithParent;
};

export const rule: GraphQLESLintRule = {
meta: {
type: 'problem',
Expand Down Expand Up @@ -105,7 +102,7 @@ export const rule: GraphQLESLintRule = {
node.name.value === 'literal' &&
node.value.kind === Kind.ENUM &&
node.value.value.indexOf('_FISCAL_') > 0 &&
isInFilter(node as NodeWithParent)
isInFilter(node)
) {
context.report({
messageId: NO_FISCAL_DATE_FILTER_SUPPORTED_RULE_ID,
Expand All @@ -127,7 +124,7 @@ export const rule: GraphQLESLintRule = {
// Checks if it is a fiscal date filter, for example 'last_n_fiscal_quarters', 'n_fiscal_years_ago'.
if (
rangeObjectField.name.value.indexOf('_fiscal_') > 0 &&
isInFilter(rangeObjectField as NodeWithParent)
isInFilter(rangeObjectField)
) {
context.report({
messageId: NO_FISCAL_DATE_FILTER_SUPPORTED_RULE_ID,
Expand All @@ -145,9 +142,11 @@ export const rule: GraphQLESLintRule = {
}
};

function isInFilter(node: NodeWithParent): boolean {
if (node.kind === Kind.ARGUMENT) {
return true;
function isInFilter<T extends ASTNode>(node: GraphQLESTreeNode<T>): boolean {
const argument = getClosestAncestorByType<T, ArgumentNode>(node, Kind.ARGUMENT);
if (argument === undefined) {
return false;
}
return node.parent === null ? false : isInFilter(node.parent);

return argument.name.value === 'where';
}
53 changes: 49 additions & 4 deletions src/rules/graphql/no-more-than-3-root-entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ export const rule: GraphQLESLintRule = {
{
title: 'Correct',
code: /* GraphQL */ `
query {
uiapi {
uiapi {
query {
Contacts {
edges {
node {
Expand All @@ -51,10 +51,55 @@ export const rule: GraphQLESLintRule = {
`
},
{
title: 'Incorrect',
title: 'Correct',
code: /* GraphQL */ `
query {
query FirstQuery {
uiapi {
query {
Contact(first: 1) {
edges {
node {
Id
}
}
}
ServiceAppointment(first: 1) {
edges {
node {
Id
}
}
}
Case(first: 1) {
edges {
node {
Id
}
}
}
}
}
}
query SecondQuery {
uiapi {
query {
Contact(first: 1) {
edges {
node {
Id
}
}
}
}
}
}
`
},
{
title: 'Incorrect',
code: /* GraphQL */ `
uiapi {
query{
Contacts {
edges {
node {
Expand Down
2 changes: 1 addition & 1 deletion src/rules/graphql/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ type NodeWithType =
| OperationTypeDefinitionNode
| VariableDefinitionNode;

type ParentNode<T> = T extends DocumentNode
export type ParentNode<T> = T extends DocumentNode
? AST.Program
: T extends DefinitionNode
? DocumentNode
Expand Down
9 changes: 3 additions & 6 deletions src/rules/graphql/unsupported-scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import { GraphQLESLintRule, GraphQLESLintRuleContext } from '@graphql-eslint/eslint-plugin';
import { Kind, NameNode } from 'graphql';
import { Kind, FieldNode } from 'graphql';

export const UNSUPPORTED_SCOPE_RULE_ID = 'offline-graphql-unsupported-scope';

Expand All @@ -14,10 +14,7 @@ export const OTHER_UNSUPPORTED_SCOPE = 'OTHER_UNSUPPORTED_SCOPE';

import getDocUrl from '../../util/getDocUrl';

type NodeWithName = {
name: NameNode;
};

import { GraphQLESTreeNode } from './types';
// key is scope name, value is the array of supported entities. Empty array means that all entities are supported.
const supportedScopes: Record<string, string[]> = {
MINE: [],
Expand Down Expand Up @@ -98,7 +95,7 @@ export const rule: GraphQLESLintRule = {
} else {
const entities = supportedScopes[scopeName];
if (entities.length > 0) {
const entityNode = node.parent as NodeWithName;
const entityNode = node.parent as GraphQLESTreeNode<FieldNode>;
if (entityNode.name.kind === Kind.NAME) {
const entityName = entityNode.name.value;
if (!entities.includes(entityName)) {
Expand Down
37 changes: 24 additions & 13 deletions src/util/graphqlAstUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

import { Position } from 'estree';
import { AST } from 'eslint';
import { GraphQLESTreeNode } from '../rules/graphql/types';
import { FieldNode, Kind, DocumentNode, OperationDefinitionNode } from 'graphql';
import { GraphQLESTreeNode, ParentNode } from '../rules/graphql/types';
import { ASTNode, FieldNode, Kind, DocumentNode, OperationDefinitionNode } from 'graphql';
import { DEFAULT_PAGE_SIZE } from '../rules/graphql/EntityStats';

export type GraphQLESFieldNode = GraphQLESTreeNode<FieldNode>;
Expand All @@ -28,17 +28,24 @@ export function getLocation(start: Position, fieldName = ''): AST.SourceLocation
}

/**
* Find closest ancestor by type
* Find closest ancestor by type. T is source ASTNode type, W is the target ASTNode type.
* @param node source node
* @param type target node type. For example, Kind.Field or Kind.Argument
*/
export function getClosestAncestorByType(
node: GraphQLESFieldNode,

export function getClosestAncestorByType<T extends ASTNode, W extends ASTNode>(
node: GraphQLESTreeNode<T>,
type: Kind
): GraphQLESFieldNode | undefined {
let parentNode: any = node.parent;
while (parentNode !== undefined && parentNode.type !== type) {
parentNode = parentNode.parent;
): GraphQLESTreeNode<W> | undefined {
const parentNode = node.parent;
if (parentNode === null || parentNode === undefined) {
return undefined;
}
return parentNode;
const astParentNode = parentNode as GraphQLESTreeNode<Exclude<ParentNode<T>, unknown>>;
if (astParentNode.type === type) {
return astParentNode as GraphQLESTreeNode<W>;
}
return getClosestAncestorByType(astParentNode, type);
}

/**
Expand Down Expand Up @@ -104,11 +111,12 @@ export function getPageSizeFromEntityNode(node: GraphQLESFieldNode): number {
export function getParentEntityNode(
entityNode: GraphQLESFieldNode
): GraphQLESFieldNode | undefined {
const node = getClosestAncestorByType(entityNode, Kind.FIELD);
const node = getClosestAncestorByType<FieldNode, FieldNode>(entityNode, Kind.FIELD);

if (node === undefined || node.name.value !== 'node') {
return undefined;
}
const edges = getClosestAncestorByType(node, Kind.FIELD);
const edges = getClosestAncestorByType<FieldNode, FieldNode>(node, Kind.FIELD);
if (edges == undefined || edges.name.value !== 'edges') {
return undefined;
}
Expand All @@ -135,7 +143,10 @@ export function getParentEntityNode(
}
*/
export function getOperationIndex(entityNode: GraphQLESFieldNode): number {
const operation = getClosestAncestorByType(entityNode, Kind.OPERATION_DEFINITION)!;
const operation = getClosestAncestorByType<FieldNode, OperationDefinitionNode>(
entityNode,
Kind.OPERATION_DEFINITION
)!;
const document = operation.parent.rawNode() as any as DocumentNode;
return document.definitions.indexOf(operation.rawNode() as any as OperationDefinitionNode);
}
Loading

0 comments on commit bf2fd89

Please sign in to comment.