diff --git a/package.json b/package.json index e6f7334..d230e51 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "url": "https://github.com/salesforce/eslint-plugin-lwc-mobile" }, "scripts": { - "build": "tsc", + "clean": "npx rimraf dist reports", + "build": "npm run clean && tsc", "format": "prettier --list-different \"**/*.{ts,js}\"", "format:fix": "prettier --write \"**/*.{ts,js,json}\"", "lint": "eslint src test", diff --git a/src/configs/recommended.ts b/src/configs/recommended.ts index f9f3867..3fea19e 100644 --- a/src/configs/recommended.ts +++ b/src/configs/recommended.ts @@ -5,11 +5,17 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ import type { ClassicConfig } from '@typescript-eslint/utils/ts-eslint'; +import { APEX_IMPORT_RULE_ID } from '../rules/apex/apex-import.js'; +import { NO_MUTATION_SUPPORTED_RULE_ID } from '../rules/graphql/no-mutation-supported'; +import { NO_AGGREGATE_QUERY_SUPPORTED_RULE_ID } from '../rules/graphql/no-aggregate-query-supported'; +import { UNSUPPORTED_SCOPE_RULE_ID } from '../rules/graphql/unsupported-scope.js'; +import { NO_SEMI_ANTI_JOIN_SUPPORTED_RULE_ID } from '../rules/graphql/no-semi-anti-join-supported'; +import { createScopedModuleRuleName } from '../util/createScopedModuleRuleName.js'; export = { extends: ['./configs/base'], rules: { - '@salesforce/lwc-mobile/apex-import': 'warn' + [createScopedModuleRuleName(APEX_IMPORT_RULE_ID)]: 'warn' }, overrides: [ { @@ -24,9 +30,10 @@ export = { skipGraphQLConfig: true }, rules: { - '@salesforce/lwc-mobile/offline-graphql-no-aggregate-query-supported': 'warn', - '@salesforce/lwc-mobile/offline-graphql-no-mutation-supported': 'warn', - '@salesforce/lwc-mobile/offline-graphql-unsupported-scope': 'warn' + [createScopedModuleRuleName(NO_MUTATION_SUPPORTED_RULE_ID)]: 'warn', + [createScopedModuleRuleName(NO_AGGREGATE_QUERY_SUPPORTED_RULE_ID)]: 'warn', + [createScopedModuleRuleName(UNSUPPORTED_SCOPE_RULE_ID)]: 'warn', + [createScopedModuleRuleName(NO_SEMI_ANTI_JOIN_SUPPORTED_RULE_ID)]: 'warn' } } ] diff --git a/src/rules/graphql/no-semi-anti-join-supported.ts b/src/rules/graphql/no-semi-anti-join-supported.ts new file mode 100644 index 0000000..691630f --- /dev/null +++ b/src/rules/graphql/no-semi-anti-join-supported.ts @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2024, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: MIT + * 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 getDocUrl from '../../util/getDocUrl'; + +export const NO_SEMI_ANTI_JOIN_SUPPORTED_RULE_ID = 'offline-graphql-no-semi-anti-join-supported'; + +export const rule: GraphQLESLintRule = { + meta: { + type: 'problem', + hasSuggestions: false, + docs: { + description: 'Semi and anti join are not supported for mobile offline', + category: 'Operations', + recommended: true, + url: getDocUrl(NO_SEMI_ANTI_JOIN_SUPPORTED_RULE_ID), + examples: [ + { + title: 'Correct', + code: /* GraphQL */ ` + query AccountExample { + uiapi { + query { + Account { + edges { + node { + Id + Name { + value + } + } + } + } + } + } + } + ` + }, + { + title: 'Incorrect', + code: /* GraphQL */ ` + query AccountExample { + uiapi { + query { + Account (where: { + Id: { inq: { + Opportunity: { + StageName: { eq: "Closed Won" } }, + ApiName:"AccountId" + } + } + }) { + edges { + node { + Id + Name { value } + } + } + } + } + } + } + } + ` + }, + { + title: 'Incorrect', + code: /* GraphQL */ ` + query AccountExample { + uiapi { + query { + Account (where: { + Id: { ninq: { + Opportunity: { + StageName: { eq: "Closed Won" } }, + ApiName:"AccountId" + } + } + }) { + edges { + node { + Id + Name { value } + } + } + } + } + } + } + } + ` + } + ] + }, + messages: { + [NO_SEMI_ANTI_JOIN_SUPPORTED_RULE_ID]: + 'Offline GraphQL: "{{joinType}}" join is not supported for mobile offline.' + }, + schema: [] + }, + + create(context: GraphQLESLintRuleContext) { + return { + ObjectField(node) { + if (node.name.value === 'inq') { + context.report({ + node: node.name, + messageId: NO_SEMI_ANTI_JOIN_SUPPORTED_RULE_ID, + data: { + joinType: 'Semi' + } + }); + } else if (node.name.value === 'ninq') { + context.report({ + node: node.name, + messageId: NO_SEMI_ANTI_JOIN_SUPPORTED_RULE_ID, + data: { + joinType: 'Anti' + } + }); + } + } + }; + } +}; diff --git a/src/util/createScopedModuleRuleName.ts b/src/util/createScopedModuleRuleName.ts new file mode 100644 index 0000000..7009767 --- /dev/null +++ b/src/util/createScopedModuleRuleName.ts @@ -0,0 +1,3 @@ +export function createScopedModuleRuleName(ruleName: string): string { + return `@salesforce/lwc-mobile/${ruleName}`; +} diff --git a/test/rules/apex/apex-import.ts b/test/rules/apex/apex-import.ts index 18afd2f..2487f0b 100644 --- a/test/rules/apex/apex-import.ts +++ b/test/rules/apex/apex-import.ts @@ -8,12 +8,13 @@ import { RuleTester } from '@typescript-eslint/rule-tester'; import { rule, APEX_IMPORT_RULE_ID } from '../../../src/rules/apex/apex-import'; +import { createScopedModuleRuleName } from '../../../src/util/createScopedModuleRuleName'; const ruleTester = new RuleTester({ parser: '@typescript-eslint/parser' }); -ruleTester.run('@salesforce/lwc-mobile/apex/apex-import', rule, { +ruleTester.run(createScopedModuleRuleName(APEX_IMPORT_RULE_ID), rule, { valid: [ { code: ` diff --git a/test/rules/graphql/no-aggregate-query-supported.spec.ts b/test/rules/graphql/no-aggregate-query-supported.spec.ts index 43bf7bf..56db056 100644 --- a/test/rules/graphql/no-aggregate-query-supported.spec.ts +++ b/test/rules/graphql/no-aggregate-query-supported.spec.ts @@ -1,14 +1,12 @@ -import { RuleTester } from '@typescript-eslint/rule-tester'; import { rule, NO_AGGREGATE_QUERY_SUPPORTED_RULE_ID } from '../../../src/rules/graphql/no-aggregate-query-supported'; +import { createScopedModuleRuleName } from '../../../src/util/createScopedModuleRuleName'; -import { RULE_TESTER_CONFIG } from '../../shared'; +import { ruleTester } from '../../shared'; -const ruleTester = new RuleTester(RULE_TESTER_CONFIG); - -ruleTester.run('@salesforce/lwc-mobile/no-aggregate-query-supported', rule as any, { +ruleTester.run(createScopedModuleRuleName(NO_AGGREGATE_QUERY_SUPPORTED_RULE_ID), rule as any, { valid: [ { code: /* GraphQL */ ` diff --git a/test/rules/graphql/no-mutation-supported.spec.ts b/test/rules/graphql/no-mutation-supported.spec.ts index 035e001..a38ceac 100644 --- a/test/rules/graphql/no-mutation-supported.spec.ts +++ b/test/rules/graphql/no-mutation-supported.spec.ts @@ -1,14 +1,12 @@ -import { RuleTester } from '@typescript-eslint/rule-tester'; import { rule, NO_MUTATION_SUPPORTED_RULE_ID } from '../../../src/rules/graphql/no-mutation-supported'; +import { createScopedModuleRuleName } from '../../../src/util/createScopedModuleRuleName'; -import { RULE_TESTER_CONFIG } from '../../shared'; +import { ruleTester } from '../../shared'; -const ruleTester = new RuleTester(RULE_TESTER_CONFIG); - -ruleTester.run('@salesforce/lwc-mobile/no-mutation-supported', rule as any, { +ruleTester.run(createScopedModuleRuleName(NO_MUTATION_SUPPORTED_RULE_ID), rule as any, { valid: [ { code: /* GraphQL */ ` diff --git a/test/rules/graphql/no-semi-anti-join-supported.spec.ts b/test/rules/graphql/no-semi-anti-join-supported.spec.ts new file mode 100644 index 0000000..4138b2c --- /dev/null +++ b/test/rules/graphql/no-semi-anti-join-supported.spec.ts @@ -0,0 +1,108 @@ +import { + rule, + NO_SEMI_ANTI_JOIN_SUPPORTED_RULE_ID +} from '../../../src/rules/graphql/no-semi-anti-join-supported'; +import { createScopedModuleRuleName } from '../../../src/util/createScopedModuleRuleName'; + +import { ruleTester } from '../../shared'; + +ruleTester.run(createScopedModuleRuleName(NO_SEMI_ANTI_JOIN_SUPPORTED_RULE_ID), rule as any, { + valid: [ + { + code: /* GraphQL */ ` + query AccountExample { + uiapi { + query { + Account { + edges { + node { + Id + Name { + value + } + } + } + } + } + } + } + ` + } + ], + invalid: [ + { + code: /* GraphQL */ ` + query AccountExample { + uiapi { + query { + Account( + where: { + Id: { + inq: { + Opportunity: { StageName: { eq: "Closed Won" } } + ApiName: "AccountId" + } + } + } + ) { + edges { + node { + Id + Name { + value + } + } + } + } + } + } + } + `, + errors: [ + { + messageId: NO_SEMI_ANTI_JOIN_SUPPORTED_RULE_ID, + data: { + joinType: 'Semi' + } + } + ] + }, + { + code: /* GraphQL */ ` + query AccountExample { + uiapi { + query { + Account( + where: { + Id: { + ninq: { + Opportunity: { StageName: { eq: "Closed Won" } } + ApiName: "AccountId" + } + } + } + ) { + edges { + node { + Id + Name { + value + } + } + } + } + } + } + } + `, + errors: [ + { + messageId: NO_SEMI_ANTI_JOIN_SUPPORTED_RULE_ID, + data: { + joinType: 'Anti' + } + } + ] + } + ] +}); diff --git a/test/rules/graphql/unsupported-scope.spec.ts b/test/rules/graphql/unsupported-scope.spec.ts index bf98c8e..49e2025 100644 --- a/test/rules/graphql/unsupported-scope.spec.ts +++ b/test/rules/graphql/unsupported-scope.spec.ts @@ -1,12 +1,9 @@ -import { RuleTester } from '@typescript-eslint/rule-tester'; import { rule, SCOPE_SUPPORTED_FOR_CERTAIN_ENTITIES_ONLY, OTHER_UNSUPPORTED_SCOPE } from '../../../src/rules/graphql/unsupported-scope'; -import { RULE_TESTER_CONFIG } from '../../shared'; - -const ruleTester = new RuleTester(RULE_TESTER_CONFIG); +import { ruleTester } from '../../shared'; ruleTester.run('@salesforce/lwc-mobile/offline-graphql-unsupported-scope', rule as any, { valid: [ diff --git a/test/shared.ts b/test/shared.ts index a732a85..54d12a3 100644 --- a/test/shared.ts +++ b/test/shared.ts @@ -1,6 +1,10 @@ -export const RULE_TESTER_CONFIG = { +import { RuleTester } from '@typescript-eslint/rule-tester'; + +const RULE_TESTER_CONFIG = { parser: '@graphql-eslint/eslint-plugin', parserOptions: { graphQLConfig: {} } }; + +export const ruleTester = new RuleTester(RULE_TESTER_CONFIG);