diff --git a/packages/@n8n/eslint-plugin-community-nodes/README.md b/packages/@n8n/eslint-plugin-community-nodes/README.md index e9f96201f83..db539c78ff6 100644 --- a/packages/@n8n/eslint-plugin-community-nodes/README.md +++ b/packages/@n8n/eslint-plugin-community-nodes/README.md @@ -83,7 +83,7 @@ export default [ | [require-node-api-error](docs/rules/require-node-api-error.md) | Require NodeApiError or NodeOperationError for error wrapping in catch blocks. Raw errors lose HTTP context in the n8n UI. | ✅ ☑️ | | | | | | [require-node-description-fields](docs/rules/require-node-description-fields.md) | Node class description must define all required fields: icon, subtitle | ✅ ☑️ | | | | | | [require-version](docs/rules/require-version.md) | Require a valid "version" field in community node package.json | ✅ ☑️ | | | | | -| [resource-operation-pattern](docs/rules/resource-operation-pattern.md) | Enforce proper resource/operation pattern for better UX in n8n nodes | | ✅ ☑️ | | | | +| [resource-operation-pattern](docs/rules/resource-operation-pattern.md) | Enforce proper resource/operation pattern for better UX in n8n nodes | ✅ ☑️ | | | | | | [trigger-node-conventions](docs/rules/trigger-node-conventions.md) | Trigger nodes (class name ends with `Trigger`) must label themselves consistently as triggers | ✅ ☑️ | | | | | | [valid-author](docs/rules/valid-author.md) | Require a non-empty author name and email in package.json | ✅ ☑️ | | | | | | [valid-credential-references](docs/rules/valid-credential-references.md) | Ensure credentials referenced in node descriptions exist as credential classes in the package | ✅ ☑️ | | | 💡 | | diff --git a/packages/@n8n/eslint-plugin-community-nodes/docs/rules/resource-operation-pattern.md b/packages/@n8n/eslint-plugin-community-nodes/docs/rules/resource-operation-pattern.md index 3d8df41f352..e7ef9f17cd0 100644 --- a/packages/@n8n/eslint-plugin-community-nodes/docs/rules/resource-operation-pattern.md +++ b/packages/@n8n/eslint-plugin-community-nodes/docs/rules/resource-operation-pattern.md @@ -1,12 +1,12 @@ # Enforce proper resource/operation pattern for better UX in n8n nodes (`@n8n/community-nodes/resource-operation-pattern`) -⚠️ This rule _warns_ in the following configs: ✅ `recommended`, ☑️ `recommendedWithoutN8nCloudSupport`. +💼 This rule is enabled in the following configs: ✅ `recommended`, ☑️ `recommendedWithoutN8nCloudSupport`. ## Rule Details -Warns when a node has more than 5 operations without organizing them into resources. The resource/operation pattern improves user experience by grouping related operations together, making complex nodes easier to navigate. +Reports an error when a node has more than 5 operations without resources. Use resources to group related operations. This pattern makes complex nodes easier to navigate. When you have many operations, users benefit from having them organized into logical resource groups (e.g., "User", "Project", "File") rather than seeing a long flat list of operations. diff --git a/packages/@n8n/eslint-plugin-community-nodes/src/plugin.ts b/packages/@n8n/eslint-plugin-community-nodes/src/plugin.ts index ac8033d871b..622119334fb 100644 --- a/packages/@n8n/eslint-plugin-community-nodes/src/plugin.ts +++ b/packages/@n8n/eslint-plugin-community-nodes/src/plugin.ts @@ -44,7 +44,7 @@ const configs = { '@n8n/community-nodes/no-unsafe-connection-type-cast': 'error', '@n8n/community-nodes/icon-validation': 'error', '@n8n/community-nodes/icon-prefer-themed-variants': 'warn', - '@n8n/community-nodes/resource-operation-pattern': 'warn', + '@n8n/community-nodes/resource-operation-pattern': 'error', '@n8n/community-nodes/trigger-node-conventions': 'error', '@n8n/community-nodes/credential-documentation-url': 'error', '@n8n/community-nodes/cred-class-field-icon-missing': 'error', @@ -95,7 +95,7 @@ const configs = { '@n8n/community-nodes/icon-validation': 'error', '@n8n/community-nodes/icon-prefer-themed-variants': 'warn', '@n8n/community-nodes/credential-documentation-url': 'error', - '@n8n/community-nodes/resource-operation-pattern': 'warn', + '@n8n/community-nodes/resource-operation-pattern': 'error', '@n8n/community-nodes/trigger-node-conventions': 'error', '@n8n/community-nodes/cred-class-field-icon-missing': 'error', '@n8n/community-nodes/cred-class-oauth2-naming': 'error', diff --git a/packages/@n8n/eslint-plugin-community-nodes/src/rules/resource-operation-pattern.test.ts b/packages/@n8n/eslint-plugin-community-nodes/src/rules/resource-operation-pattern.test.ts index db29194820e..2a7934783e4 100644 --- a/packages/@n8n/eslint-plugin-community-nodes/src/rules/resource-operation-pattern.test.ts +++ b/packages/@n8n/eslint-plugin-community-nodes/src/rules/resource-operation-pattern.test.ts @@ -1,5 +1,8 @@ import { RuleTester } from '@typescript-eslint/rule-tester'; +import { Linter } from 'eslint'; +import { describe, expect, it } from 'vitest'; +import { configs } from '../plugin.js'; import { ResourceOperationPatternRule } from './resource-operation-pattern.js'; const ruleTester = new RuleTester(); @@ -215,3 +218,61 @@ ruleTester.run('resource-operation-pattern', ResourceOperationPatternRule, { }, ], }); + +const resourceOperationRuleId = '@n8n/community-nodes/resource-operation-pattern'; + +function nodeWithSixOperations(hasResource: boolean): string { + return ` + class TestNode extends Node { + description = { + properties: [ + ${hasResource ? "{ name: 'resource', type: 'options', options: [{ name: 'User', value: 'user' }] }," : ''} + { + name: 'operation', + type: 'options', + options: [ + { name: 'Get', value: 'get' }, + { name: 'Create', value: 'create' }, + { name: 'Update', value: 'update' }, + { name: 'Delete', value: 'delete' }, + { name: 'List', value: 'list' }, + { name: 'Search', value: 'search' }, + ], + }, + ], + }; + } + `; +} + +describe.each([ + ['recommended', configs.recommended], + ['recommendedWithoutN8nCloudSupport', configs.recommendedWithoutN8nCloudSupport], +] as const)('%s config', (_name, config) => { + const linter = new Linter(); + const lint = (code: string) => + linter.verify( + code, + { + files: ['**/*.node.ts'], + plugins: config.plugins, + rules: { [resourceOperationRuleId]: config.rules[resourceOperationRuleId] }, + }, + { filename: 'TestNode.node.ts' }, + ); + + // CE-3284: The resource finding must fail lint even when it is the only finding. + it('reports missing resources as the only error', () => { + expect(lint(nodeWithSixOperations(false))).toEqual([ + expect.objectContaining({ + ruleId: resourceOperationRuleId, + messageId: 'tooManyOperationsWithoutResources', + severity: 2, + }), + ]); + }); + + it('accepts operations organized by resource', () => { + expect(lint(nodeWithSixOperations(true))).toEqual([]); + }); +});