diff --git a/jest.config.ts b/jest.config.ts index 895dd9f..d9490c2 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -14,7 +14,7 @@ const config: Config = { testMatch: ['/test/**/*.ts'], moduleFileExtensions: ['ts', 'js', 'json'], testResultsProcessor: 'jest-sonar-reporter', - testPathIgnorePatterns: ['/node_modules/', '/lib/'], + testPathIgnorePatterns: ['/node_modules/', '/lib/', '/test/shared.ts'], moduleDirectories: ['node_modules'], collectCoverage: true, collectCoverageFrom: ['src/**/*.ts', '!src/index.ts', '!src/configs/*.ts'], diff --git a/src/configs/recommended.ts b/src/configs/recommended.ts index 6237f34..f9f3867 100644 --- a/src/configs/recommended.ts +++ b/src/configs/recommended.ts @@ -8,6 +8,9 @@ import type { ClassicConfig } from '@typescript-eslint/utils/ts-eslint'; export = { extends: ['./configs/base'], + rules: { + '@salesforce/lwc-mobile/apex-import': 'warn' + }, overrides: [ { files: ['*.js'], @@ -21,9 +24,9 @@ export = { skipGraphQLConfig: true }, rules: { - '@salesforce/lwc-mobile/apex-import': 'warn', + '@salesforce/lwc-mobile/offline-graphql-no-aggregate-query-supported': 'warn', '@salesforce/lwc-mobile/offline-graphql-no-mutation-supported': 'warn', - '@salesforce/lwc-mobile/offline-graphql-no-aggregate-query-supported': 'warn' + '@salesforce/lwc-mobile/offline-graphql-unsupported-scope': 'warn' } } ] diff --git a/src/index.ts b/src/index.ts index 4a54b50..967fbb1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,6 +19,11 @@ import { rule as mutationNotSupported } from './rules/graphql/no-mutation-supported.js'; +import { + UNSUPPORTED_SCOPE_RULE_ID, + rule as unsupportedScope +} from './rules/graphql/unsupported-scope.js'; + import { name, version } from '../package.json'; export = { @@ -33,7 +38,8 @@ export = { rules: { 'enforce-foo-bar': enforceFooBar, [APEX_IMPORT_RULE_ID]: apexImport, + [NO_AGGREGATE_QUERY_SUPPORTED_RULE_ID]: aggregateQueryNotSupported, [NO_MUTATION_SUPPORTED_RULE_ID]: mutationNotSupported, - [NO_AGGREGATE_QUERY_SUPPORTED_RULE_ID]: aggregateQueryNotSupported + [UNSUPPORTED_SCOPE_RULE_ID]: unsupportedScope } }; diff --git a/src/rules/graphql/unsupported-scope.ts b/src/rules/graphql/unsupported-scope.ts new file mode 100644 index 0000000..25e952c --- /dev/null +++ b/src/rules/graphql/unsupported-scope.ts @@ -0,0 +1,118 @@ +import { GraphQLESLintRule, GraphQLESLintRuleContext } from '@graphql-eslint/eslint-plugin'; +import { Kind, NameNode } from 'graphql'; + +export const UNSUPPORTED_SCOPE_RULE_ID = 'offline-graphql-unsupported-scope'; + +export const SCOPE_SUPPORTED_FOR_CERTAIN_ENTITIES_ONLY = 'ASSIGNED_TO_ME__SERVICEAPPOINTMENT_ONLY'; +export const OTHER_UNSUPPORTED_SCOPE = 'OTHER_UNSUPPORTED_SCOPE'; + +import getDocUrl from '../../util/getDocUrl'; + +type NodeWithName = { + name: NameNode; +}; + +// key is scope name, value is the array of supported entities. Empty array means that all entities are supported. +const supportedScopes: Record = { + MINE: [], + ASSIGNEDTOME: ['ServiceAppointment'] +}; +export const rule: GraphQLESLintRule = { + meta: { + type: 'problem', + docs: { + category: 'Operations', + description: `For mobile offline use cases, scope "MINE" is supported and scope "ASSIGNEDTOME" is only supported for ServiceAppointment . All other scopes like TEAM, QUEUE_OWNED and USER_OWNED are not supported `, + url: getDocUrl(UNSUPPORTED_SCOPE_RULE_ID), + recommended: true, + examples: [ + { + title: 'Incorrect', + code: /* GraphQL */ ` + query scopeQuery { + uiapi { + query { + Case(scope: EVERYTHING) { + edges { + node { + Id + } + } + } + } + } + } + ` + }, + { + title: 'Incorrect', + code: /* GraphQL */ ` + query assignedtomeQuery { + uiapi { + query { + Case(scope: ASSIGNEDTOME) { + edges { + node { + Id + } + } + } + } + } + } + ` + } + ] + }, + messages: { + [SCOPE_SUPPORTED_FOR_CERTAIN_ENTITIES_ONLY]: + 'Offline GraphQL: Scope "{{scopeName}}" is only supported for the "{{supportedEntities}}" entity, for mobile offline use cases', + [OTHER_UNSUPPORTED_SCOPE]: + 'Offline GraphQL: Scope "{{scopeName}}" is unsupported for mobile offline use cases.' + }, + schema: [] + }, + + create(context: GraphQLESLintRuleContext) { + return { + Argument(node) { + if (node.name.value === 'scope' && node.value.kind === Kind.ENUM) { + const scopeName = node.value.value; + if (supportedScopes[scopeName] === undefined) { + context.report({ + messageId: OTHER_UNSUPPORTED_SCOPE, + data: { + scopeName + }, + loc: { + start: node.loc.start, + end: node.value.loc.end + } + }); + } else { + const entities = supportedScopes[scopeName]; + if (entities.length > 0) { + const entityNode = node.parent as NodeWithName; + if (entityNode.name.kind === Kind.NAME) { + const entityName = entityNode.name.value; + if (!entities.includes(entityName)) { + context.report({ + messageId: SCOPE_SUPPORTED_FOR_CERTAIN_ENTITIES_ONLY, + loc: { + start: node.loc.start, + end: node.value.loc.end + }, + data: { + scopeName, + supportedEntities: entities.join(', ') + } + }); + } + } + } + } + } + } + }; + } +}; diff --git a/test/rules/graphql/no-aggregate-query-supported.spec.ts b/test/rules/graphql/no-aggregate-query-supported.spec.ts index a034b7e..43bf7bf 100644 --- a/test/rules/graphql/no-aggregate-query-supported.spec.ts +++ b/test/rules/graphql/no-aggregate-query-supported.spec.ts @@ -4,12 +4,9 @@ import { NO_AGGREGATE_QUERY_SUPPORTED_RULE_ID } from '../../../src/rules/graphql/no-aggregate-query-supported'; -const ruleTester = new RuleTester({ - parser: '@graphql-eslint/eslint-plugin', - parserOptions: { - graphQLConfig: {} - } -}); +import { RULE_TESTER_CONFIG } from '../../shared'; + +const ruleTester = new RuleTester(RULE_TESTER_CONFIG); ruleTester.run('@salesforce/lwc-mobile/no-aggregate-query-supported', rule as any, { valid: [ diff --git a/test/rules/graphql/no-mutation-supported.spec.ts b/test/rules/graphql/no-mutation-supported.spec.ts index 04f681c..035e001 100644 --- a/test/rules/graphql/no-mutation-supported.spec.ts +++ b/test/rules/graphql/no-mutation-supported.spec.ts @@ -4,12 +4,9 @@ import { NO_MUTATION_SUPPORTED_RULE_ID } from '../../../src/rules/graphql/no-mutation-supported'; -const ruleTester = new RuleTester({ - parser: '@graphql-eslint/eslint-plugin', - parserOptions: { - graphQLConfig: {} - } -}); +import { RULE_TESTER_CONFIG } from '../../shared'; + +const ruleTester = new RuleTester(RULE_TESTER_CONFIG); ruleTester.run('@salesforce/lwc-mobile/no-mutation-supported', rule as any, { valid: [ diff --git a/test/rules/graphql/unsupported-scope.spec.ts b/test/rules/graphql/unsupported-scope.spec.ts new file mode 100644 index 0000000..bf98c8e --- /dev/null +++ b/test/rules/graphql/unsupported-scope.spec.ts @@ -0,0 +1,90 @@ +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); + +ruleTester.run('@salesforce/lwc-mobile/offline-graphql-unsupported-scope', rule as any, { + valid: [ + { + code: /* GraphQL */ ` + query scopeQuery { + uiapi { + query { + ServiceAppointment(first: 20, scope: ASSIGNEDTOME) { + edges { + node { + Id + Name { + value + } + } + } + } + } + } + } + ` + } + ], + invalid: [ + { + code: /* GraphQL */ ` + query scopeQuery { + uiapi { + query { + Case(scope: EVERYTHING) { + edges { + node { + Id + } + } + } + } + } + } + `, + errors: [ + { + messageId: OTHER_UNSUPPORTED_SCOPE, + data: { + scopeName: 'EVERYTHING' + } + } + ] + }, + { + code: /* GraphQL */ ` + query scopeQuery { + uiapi { + query { + Case(first: 20, scope: ASSIGNEDTOME) { + edges { + node { + Id + Name { + value + } + } + } + } + } + } + } + `, + errors: [ + { + messageId: SCOPE_SUPPORTED_FOR_CERTAIN_ENTITIES_ONLY, + data: { + scopeName: 'ASSIGNEDTOME', + supportedEntities: 'ServiceAppointment' + } + } + ] + } + ] +}); diff --git a/test/shared.ts b/test/shared.ts new file mode 100644 index 0000000..a732a85 --- /dev/null +++ b/test/shared.ts @@ -0,0 +1,6 @@ +export const RULE_TESTER_CONFIG = { + parser: '@graphql-eslint/eslint-plugin', + parserOptions: { + graphQLConfig: {} + } +};