@n8n/eslint-plugin-community-nodes 0.35.1 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -77,13 +77,14 @@ export default [
77
77
  | [node-connection-type-literal](docs/rules/node-connection-type-literal.md) | Disallow string literals in node description `inputs`/`outputs` — use `NodeConnectionTypes` enum instead | ✅ ☑️ | | 🔧 | | |
78
78
  | [node-registration-complete](docs/rules/node-registration-complete.md) | Ensure every `.node.ts` file in the `nodes/` directory is registered in the "n8n.nodes" array of package.json | | ✅ ☑️ | | | |
79
79
  | [node-usable-as-tool](docs/rules/node-usable-as-tool.md) | Ensure node classes have usableAsTool property | ✅ ☑️ | | 🔧 | | |
80
+ | [open-ended-node-fields](docs/rules/open-ended-node-fields.md) | Discourage optional open-ended JSON fields in node properties | | ✅ ☑️ | | | |
80
81
  | [package-name-convention](docs/rules/package-name-convention.md) | Enforce correct package naming convention for n8n community nodes | ✅ ☑️ | | | 💡 | |
81
82
  | [require-files-array](docs/rules/require-files-array.md) | Require a non-empty "files" array in package.json | | ✅ ☑️ | | | |
82
83
  | [require-homepage](docs/rules/require-homepage.md) | Require a "homepage" field with a valid URL in package.json | | ✅ ☑️ | | | |
83
84
  | [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. | ✅ ☑️ | | | | |
84
85
  | [require-node-description-fields](docs/rules/require-node-description-fields.md) | Node class description must define all required fields: icon, subtitle | ✅ ☑️ | | | | |
85
86
  | [require-version](docs/rules/require-version.md) | Require a valid "version" field in community node package.json | ✅ ☑️ | | | | |
86
- | [resource-operation-pattern](docs/rules/resource-operation-pattern.md) | Enforce proper resource/operation pattern for better UX in n8n nodes | | ✅ ☑️ | | | |
87
+ | [resource-operation-pattern](docs/rules/resource-operation-pattern.md) | Enforce proper resource/operation pattern for better UX in n8n nodes | ✅ ☑️ | | | | |
87
88
  | [trigger-node-conventions](docs/rules/trigger-node-conventions.md) | Trigger nodes (class name ends with `Trigger`) must label themselves consistently as triggers | ✅ ☑️ | | | | |
88
89
  | [valid-author](docs/rules/valid-author.md) | Require a non-empty author name and email in package.json | ✅ ☑️ | | | | |
89
90
  | [valid-credential-references](docs/rules/valid-credential-references.md) | Ensure credentials referenced in node descriptions exist as credential classes in the package | ✅ ☑️ | | | 💡 | |
package/dist/plugin.d.ts CHANGED
@@ -23,6 +23,7 @@ declare const configs: {
23
23
  '@n8n/community-nodes/no-deprecated-workflow-functions': "error";
24
24
  '@n8n/community-nodes/no-emoji-in-options': "error";
25
25
  '@n8n/community-nodes/node-usable-as-tool': "error";
26
+ '@n8n/community-nodes/open-ended-node-fields': "warn";
26
27
  '@n8n/community-nodes/package-name-convention': "error";
27
28
  '@n8n/community-nodes/credential-test-required': "error";
28
29
  '@n8n/community-nodes/no-credential-reuse': "error";
@@ -38,7 +39,7 @@ declare const configs: {
38
39
  '@n8n/community-nodes/no-unsafe-connection-type-cast': "error";
39
40
  '@n8n/community-nodes/icon-validation': "error";
40
41
  '@n8n/community-nodes/icon-prefer-themed-variants': "warn";
41
- '@n8n/community-nodes/resource-operation-pattern': "warn";
42
+ '@n8n/community-nodes/resource-operation-pattern': "error";
42
43
  '@n8n/community-nodes/trigger-node-conventions': "error";
43
44
  '@n8n/community-nodes/credential-documentation-url': "error";
44
45
  '@n8n/community-nodes/cred-class-field-icon-missing': "error";
@@ -80,6 +81,7 @@ declare const configs: {
80
81
  '@n8n/community-nodes/no-deprecated-workflow-functions': "error";
81
82
  '@n8n/community-nodes/no-emoji-in-options': "error";
82
83
  '@n8n/community-nodes/node-usable-as-tool': "error";
84
+ '@n8n/community-nodes/open-ended-node-fields': "warn";
83
85
  '@n8n/community-nodes/package-name-convention': "error";
84
86
  '@n8n/community-nodes/credential-test-required': "error";
85
87
  '@n8n/community-nodes/no-credential-reuse': "error";
@@ -96,7 +98,7 @@ declare const configs: {
96
98
  '@n8n/community-nodes/icon-validation': "error";
97
99
  '@n8n/community-nodes/icon-prefer-themed-variants': "warn";
98
100
  '@n8n/community-nodes/credential-documentation-url': "error";
99
- '@n8n/community-nodes/resource-operation-pattern': "warn";
101
+ '@n8n/community-nodes/resource-operation-pattern': "error";
100
102
  '@n8n/community-nodes/trigger-node-conventions': "error";
101
103
  '@n8n/community-nodes/cred-class-field-icon-missing': "error";
102
104
  '@n8n/community-nodes/cred-class-oauth2-naming': "error";
@@ -148,6 +150,7 @@ declare const pluginWithConfigs: {
148
150
  '@n8n/community-nodes/no-deprecated-workflow-functions': "error";
149
151
  '@n8n/community-nodes/no-emoji-in-options': "error";
150
152
  '@n8n/community-nodes/node-usable-as-tool': "error";
153
+ '@n8n/community-nodes/open-ended-node-fields': "warn";
151
154
  '@n8n/community-nodes/package-name-convention': "error";
152
155
  '@n8n/community-nodes/credential-test-required': "error";
153
156
  '@n8n/community-nodes/no-credential-reuse': "error";
@@ -163,7 +166,7 @@ declare const pluginWithConfigs: {
163
166
  '@n8n/community-nodes/no-unsafe-connection-type-cast': "error";
164
167
  '@n8n/community-nodes/icon-validation': "error";
165
168
  '@n8n/community-nodes/icon-prefer-themed-variants': "warn";
166
- '@n8n/community-nodes/resource-operation-pattern': "warn";
169
+ '@n8n/community-nodes/resource-operation-pattern': "error";
167
170
  '@n8n/community-nodes/trigger-node-conventions': "error";
168
171
  '@n8n/community-nodes/credential-documentation-url': "error";
169
172
  '@n8n/community-nodes/cred-class-field-icon-missing': "error";
@@ -205,6 +208,7 @@ declare const pluginWithConfigs: {
205
208
  '@n8n/community-nodes/no-deprecated-workflow-functions': "error";
206
209
  '@n8n/community-nodes/no-emoji-in-options': "error";
207
210
  '@n8n/community-nodes/node-usable-as-tool': "error";
211
+ '@n8n/community-nodes/open-ended-node-fields': "warn";
208
212
  '@n8n/community-nodes/package-name-convention': "error";
209
213
  '@n8n/community-nodes/credential-test-required': "error";
210
214
  '@n8n/community-nodes/no-credential-reuse': "error";
@@ -221,7 +225,7 @@ declare const pluginWithConfigs: {
221
225
  '@n8n/community-nodes/icon-validation': "error";
222
226
  '@n8n/community-nodes/icon-prefer-themed-variants': "warn";
223
227
  '@n8n/community-nodes/credential-documentation-url': "error";
224
- '@n8n/community-nodes/resource-operation-pattern': "warn";
228
+ '@n8n/community-nodes/resource-operation-pattern': "error";
225
229
  '@n8n/community-nodes/trigger-node-conventions': "error";
226
230
  '@n8n/community-nodes/cred-class-field-icon-missing': "error";
227
231
  '@n8n/community-nodes/cred-class-oauth2-naming': "error";
@@ -274,6 +278,7 @@ declare const n8nCommunityNodesPlugin: {
274
278
  '@n8n/community-nodes/no-deprecated-workflow-functions': "error";
275
279
  '@n8n/community-nodes/no-emoji-in-options': "error";
276
280
  '@n8n/community-nodes/node-usable-as-tool': "error";
281
+ '@n8n/community-nodes/open-ended-node-fields': "warn";
277
282
  '@n8n/community-nodes/package-name-convention': "error";
278
283
  '@n8n/community-nodes/credential-test-required': "error";
279
284
  '@n8n/community-nodes/no-credential-reuse': "error";
@@ -289,7 +294,7 @@ declare const n8nCommunityNodesPlugin: {
289
294
  '@n8n/community-nodes/no-unsafe-connection-type-cast': "error";
290
295
  '@n8n/community-nodes/icon-validation': "error";
291
296
  '@n8n/community-nodes/icon-prefer-themed-variants': "warn";
292
- '@n8n/community-nodes/resource-operation-pattern': "warn";
297
+ '@n8n/community-nodes/resource-operation-pattern': "error";
293
298
  '@n8n/community-nodes/trigger-node-conventions': "error";
294
299
  '@n8n/community-nodes/credential-documentation-url': "error";
295
300
  '@n8n/community-nodes/cred-class-field-icon-missing': "error";
@@ -331,6 +336,7 @@ declare const n8nCommunityNodesPlugin: {
331
336
  '@n8n/community-nodes/no-deprecated-workflow-functions': "error";
332
337
  '@n8n/community-nodes/no-emoji-in-options': "error";
333
338
  '@n8n/community-nodes/node-usable-as-tool': "error";
339
+ '@n8n/community-nodes/open-ended-node-fields': "warn";
334
340
  '@n8n/community-nodes/package-name-convention': "error";
335
341
  '@n8n/community-nodes/credential-test-required': "error";
336
342
  '@n8n/community-nodes/no-credential-reuse': "error";
@@ -347,7 +353,7 @@ declare const n8nCommunityNodesPlugin: {
347
353
  '@n8n/community-nodes/icon-validation': "error";
348
354
  '@n8n/community-nodes/icon-prefer-themed-variants': "warn";
349
355
  '@n8n/community-nodes/credential-documentation-url': "error";
350
- '@n8n/community-nodes/resource-operation-pattern': "warn";
356
+ '@n8n/community-nodes/resource-operation-pattern': "error";
351
357
  '@n8n/community-nodes/trigger-node-conventions': "error";
352
358
  '@n8n/community-nodes/cred-class-field-icon-missing': "error";
353
359
  '@n8n/community-nodes/cred-class-oauth2-naming': "error";
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AAG7C,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAYzC,QAAA,MAAM,OAAO;;;;YAIV,sBAAsB;;oBAZvB,IAAI;oBACJ,OAAO;oBACP,SAAS;;uBAGM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;YA2DpC,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+CgB,CAAC;AAE1C,QAAA,MAAM,iBAAiB;;QAjHrB,IAAI;QACJ,OAAO;QACP,SAAS;;WAGM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;;;;;gBAOpC,sBAAsB;;wBAZvB,IAAI;wBACJ,OAAO;wBACP,SAAS;;2BAGM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gBA2DpC,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiD+C,CAAC;AAEzE,QAAA,MAAM,uBAAuB;;QAnH3B,IAAI;QACJ,OAAO;QACP,SAAS;;WAGM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;;;;;gBAOpC,sBAAsB;;wBAZvB,IAAI;wBACJ,OAAO;wBACP,SAAS;;2BAGM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gBA2DpC,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmDwB,CAAC;eACnC,iBAAiB;AAChC,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,uBAAuB,EAAE,CAAC"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AAG7C,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAYzC,QAAA,MAAM,OAAO;;;;YAIV,sBAAsB;;oBAZvB,IAAI;oBACJ,OAAO;oBACP,SAAS;;uBAGM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;YA4DpC,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgDgB,CAAC;AAE1C,QAAA,MAAM,iBAAiB;;QAnHrB,IAAI;QACJ,OAAO;QACP,SAAS;;WAGM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;;;;;gBAOpC,sBAAsB;;wBAZvB,IAAI;wBACJ,OAAO;wBACP,SAAS;;2BAGM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gBA4DpC,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkD+C,CAAC;AAEzE,QAAA,MAAM,uBAAuB;;QArH3B,IAAI;QACJ,OAAO;QACP,SAAS;;WAGM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;;;;;gBAOpC,sBAAsB;;wBAZvB,IAAI;wBACJ,OAAO;wBACP,SAAS;;2BAGM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gBA4DpC,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoDwB,CAAC;eACnC,iBAAiB;AAChC,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,uBAAuB,EAAE,CAAC"}
package/dist/plugin.js CHANGED
@@ -25,6 +25,7 @@ const configs = {
25
25
  '@n8n/community-nodes/no-deprecated-workflow-functions': 'error',
26
26
  '@n8n/community-nodes/no-emoji-in-options': 'error',
27
27
  '@n8n/community-nodes/node-usable-as-tool': 'error',
28
+ '@n8n/community-nodes/open-ended-node-fields': 'warn',
28
29
  '@n8n/community-nodes/package-name-convention': 'error',
29
30
  '@n8n/community-nodes/credential-test-required': 'error',
30
31
  '@n8n/community-nodes/no-credential-reuse': 'error',
@@ -40,7 +41,7 @@ const configs = {
40
41
  '@n8n/community-nodes/no-unsafe-connection-type-cast': 'error',
41
42
  '@n8n/community-nodes/icon-validation': 'error',
42
43
  '@n8n/community-nodes/icon-prefer-themed-variants': 'warn',
43
- '@n8n/community-nodes/resource-operation-pattern': 'warn',
44
+ '@n8n/community-nodes/resource-operation-pattern': 'error',
44
45
  '@n8n/community-nodes/trigger-node-conventions': 'error',
45
46
  '@n8n/community-nodes/credential-documentation-url': 'error',
46
47
  '@n8n/community-nodes/cred-class-field-icon-missing': 'error',
@@ -75,6 +76,7 @@ const configs = {
75
76
  '@n8n/community-nodes/no-deprecated-workflow-functions': 'error',
76
77
  '@n8n/community-nodes/no-emoji-in-options': 'error',
77
78
  '@n8n/community-nodes/node-usable-as-tool': 'error',
79
+ '@n8n/community-nodes/open-ended-node-fields': 'warn',
78
80
  '@n8n/community-nodes/package-name-convention': 'error',
79
81
  '@n8n/community-nodes/credential-test-required': 'error',
80
82
  '@n8n/community-nodes/no-credential-reuse': 'error',
@@ -91,7 +93,7 @@ const configs = {
91
93
  '@n8n/community-nodes/icon-validation': 'error',
92
94
  '@n8n/community-nodes/icon-prefer-themed-variants': 'warn',
93
95
  '@n8n/community-nodes/credential-documentation-url': 'error',
94
- '@n8n/community-nodes/resource-operation-pattern': 'warn',
96
+ '@n8n/community-nodes/resource-operation-pattern': 'error',
95
97
  '@n8n/community-nodes/trigger-node-conventions': 'error',
96
98
  '@n8n/community-nodes/cred-class-field-icon-missing': 'error',
97
99
  '@n8n/community-nodes/cred-class-oauth2-naming': 'error',
@@ -20,6 +20,9 @@ export declare const rules: {
20
20
  'node-usable-as-tool': import("@typescript-eslint/utils/ts-eslint").RuleModule<"aiOnlyUsableAsTool" | "missingUsableAsTool" | "triggerUsableAsTool", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
21
21
  name: string;
22
22
  };
23
+ 'open-ended-node-fields': import("@typescript-eslint/utils/ts-eslint").RuleModule<"optionalJson", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
24
+ name: string;
25
+ };
23
26
  'package-name-convention': import("@typescript-eslint/utils/ts-eslint").RuleModule<"defaultPlaceholderName" | "invalidPackageName" | "missingName" | "renameTo", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
24
27
  name: string;
25
28
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rules/index.ts"],"names":[],"mappings":"AAgDA,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8CuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rules/index.ts"],"names":[],"mappings":"AAiDA,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+CuB,CAAC"}
@@ -29,6 +29,7 @@ import { NodeClassDescriptionIconMissingRule } from './node-class-description-ic
29
29
  import { NodeConnectionTypeLiteralRule } from './node-connection-type-literal.js';
30
30
  import { NodeRegistrationCompleteRule } from './node-registration-complete.js';
31
31
  import { NodeUsableAsToolRule } from './node-usable-as-tool.js';
32
+ import { OpenEndedNodeFieldsRule } from './open-ended-node-fields.js';
32
33
  import { PackageNameConventionRule } from './package-name-convention.js';
33
34
  import { RequireFilesArrayRule } from './require-files-array.js';
34
35
  import { RequireHomepageRule } from './require-homepage.js';
@@ -51,6 +52,7 @@ export const rules = {
51
52
  'no-deprecated-workflow-functions': NoDeprecatedWorkflowFunctionsRule,
52
53
  'no-emoji-in-options': NoEmojiInOptionsRule,
53
54
  'node-usable-as-tool': NodeUsableAsToolRule,
55
+ 'open-ended-node-fields': OpenEndedNodeFieldsRule,
54
56
  'package-name-convention': PackageNameConventionRule,
55
57
  'credential-test-required': CredentialTestRequiredRule,
56
58
  'credential-unnecessary-password': CredentialUnnecessaryPasswordRule,
@@ -0,0 +1,4 @@
1
+ export declare const OpenEndedNodeFieldsRule: import("@typescript-eslint/utils/ts-eslint").RuleModule<"optionalJson", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
2
+ name: string;
3
+ };
4
+ //# sourceMappingURL=open-ended-node-fields.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"open-ended-node-fields.d.ts","sourceRoot":"","sources":["../../src/rules/open-ended-node-fields.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,uBAAuB;;CAyClC,CAAC"}
@@ -0,0 +1,44 @@
1
+ import { AST_NODE_TYPES } from '@typescript-eslint/utils';
2
+ import { createRule, findArrayLiteralProperty, findNodeDescriptionObject, findObjectProperty, getBooleanLiteralValue, getStringLiteralValue, isFileType, isNodeTypeClass, } from '../utils/index.js';
3
+ export const OpenEndedNodeFieldsRule = createRule({
4
+ name: 'open-ended-node-fields',
5
+ meta: {
6
+ type: 'suggestion',
7
+ docs: {
8
+ description: 'Discourage optional open-ended JSON fields in node properties',
9
+ },
10
+ messages: {
11
+ optionalJson: 'Optional JSON fields can hide configuration. Use typed parameters under Additional Options or Additional Fields instead.',
12
+ },
13
+ schema: [],
14
+ },
15
+ defaultOptions: [],
16
+ create(context) {
17
+ if (!isFileType(context.filename, '.node.ts'))
18
+ return {};
19
+ return {
20
+ ClassDeclaration(node) {
21
+ if (!isNodeTypeClass(node))
22
+ return;
23
+ const description = findNodeDescriptionObject(node);
24
+ if (!description)
25
+ return;
26
+ const properties = findArrayLiteralProperty(description, 'properties');
27
+ if (!properties)
28
+ return;
29
+ for (const property of properties.elements) {
30
+ if (property?.type !== AST_NODE_TYPES.ObjectExpression)
31
+ continue;
32
+ const type = findObjectProperty(property, 'type');
33
+ if (getStringLiteralValue(type?.value ?? null) !== 'json')
34
+ continue;
35
+ const required = findObjectProperty(property, 'required');
36
+ if (getBooleanLiteralValue(required?.value ?? null) === true)
37
+ continue;
38
+ context.report({ node: property, messageId: 'optionalJson' });
39
+ }
40
+ },
41
+ };
42
+ },
43
+ });
44
+ //# sourceMappingURL=open-ended-node-fields.js.map
@@ -0,0 +1,35 @@
1
+ # Discourage optional open-ended JSON fields in node properties (`@n8n/community-nodes/open-ended-node-fields`)
2
+
3
+ ⚠️ This rule _warns_ in the following configs: ✅ `recommended`, ☑️ `recommendedWithoutN8nCloudSupport`.
4
+
5
+ <!-- end auto-generated rule header -->
6
+
7
+ ## Rule Details
8
+
9
+ This rule warns about optional top-level JSON properties in node descriptions. A JSON configuration field can hide parameters from the editor. Typed parameters make configuration easier to find, validate, and change.
10
+
11
+ Place optional parameters under Additional Options or Additional Fields. The rule does not report required JSON properties or typed collection options.
12
+
13
+ ## Examples
14
+
15
+ ### Incorrect
16
+
17
+ ```typescript
18
+ properties: [
19
+ { displayName: 'Configuration', name: 'configuration', type: 'json', default: '{}' },
20
+ ];
21
+ ```
22
+
23
+ ### Correct
24
+
25
+ ```typescript
26
+ properties: [
27
+ {
28
+ displayName: 'Additional Fields',
29
+ name: 'additionalFields',
30
+ type: 'collection',
31
+ default: {},
32
+ options: [{ displayName: 'Limit', name: 'limit', type: 'number', default: 10 }],
33
+ },
34
+ ];
35
+ ```
@@ -1,12 +1,12 @@
1
1
  # Enforce proper resource/operation pattern for better UX in n8n nodes (`@n8n/community-nodes/resource-operation-pattern`)
2
2
 
3
- ⚠️ This rule _warns_ in the following configs: ✅ `recommended`, ☑️ `recommendedWithoutN8nCloudSupport`.
3
+ 💼 This rule is enabled in the following configs: ✅ `recommended`, ☑️ `recommendedWithoutN8nCloudSupport`.
4
4
 
5
5
  <!-- end auto-generated rule header -->
6
6
 
7
7
  ## Rule Details
8
8
 
9
- 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.
9
+ 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.
10
10
 
11
11
  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.
12
12
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@n8n/eslint-plugin-community-nodes",
3
3
  "type": "module",
4
- "version": "0.35.1",
4
+ "version": "0.37.0",
5
5
  "main": "./dist/plugin.js",
6
6
  "types": "./dist/plugin.d.ts",
7
7
  "exports": {
@@ -11,7 +11,7 @@
11
11
  }
12
12
  },
13
13
  "dependencies": {
14
- "@n8n/utils": "1.50.1",
14
+ "@n8n/utils": "1.52.0",
15
15
  "@typescript-eslint/typescript-estree": "8.70.0",
16
16
  "@typescript-eslint/utils": "8.70.0",
17
17
  "fastest-levenshtein": "1.0.16"
@@ -20,7 +20,7 @@
20
20
  "@n8n/typescript-config": "1.12.0",
21
21
  "@n8n/vitest-config": "1.24.0",
22
22
  "@types/node": "24.10.1",
23
- "n8n-workflow": "2.43.2",
23
+ "n8n-workflow": "2.45.0",
24
24
  "@typescript-eslint/rule-tester": "8.70.0",
25
25
  "eslint-doc-generator": "^2.2.2",
26
26
  "eslint-plugin-eslint-plugin": "^7.0.0",
package/src/plugin.ts CHANGED
@@ -29,6 +29,7 @@ const configs = {
29
29
  '@n8n/community-nodes/no-deprecated-workflow-functions': 'error',
30
30
  '@n8n/community-nodes/no-emoji-in-options': 'error',
31
31
  '@n8n/community-nodes/node-usable-as-tool': 'error',
32
+ '@n8n/community-nodes/open-ended-node-fields': 'warn',
32
33
  '@n8n/community-nodes/package-name-convention': 'error',
33
34
  '@n8n/community-nodes/credential-test-required': 'error',
34
35
  '@n8n/community-nodes/no-credential-reuse': 'error',
@@ -44,7 +45,7 @@ const configs = {
44
45
  '@n8n/community-nodes/no-unsafe-connection-type-cast': 'error',
45
46
  '@n8n/community-nodes/icon-validation': 'error',
46
47
  '@n8n/community-nodes/icon-prefer-themed-variants': 'warn',
47
- '@n8n/community-nodes/resource-operation-pattern': 'warn',
48
+ '@n8n/community-nodes/resource-operation-pattern': 'error',
48
49
  '@n8n/community-nodes/trigger-node-conventions': 'error',
49
50
  '@n8n/community-nodes/credential-documentation-url': 'error',
50
51
  '@n8n/community-nodes/cred-class-field-icon-missing': 'error',
@@ -79,6 +80,7 @@ const configs = {
79
80
  '@n8n/community-nodes/no-deprecated-workflow-functions': 'error',
80
81
  '@n8n/community-nodes/no-emoji-in-options': 'error',
81
82
  '@n8n/community-nodes/node-usable-as-tool': 'error',
83
+ '@n8n/community-nodes/open-ended-node-fields': 'warn',
82
84
  '@n8n/community-nodes/package-name-convention': 'error',
83
85
  '@n8n/community-nodes/credential-test-required': 'error',
84
86
  '@n8n/community-nodes/no-credential-reuse': 'error',
@@ -95,7 +97,7 @@ const configs = {
95
97
  '@n8n/community-nodes/icon-validation': 'error',
96
98
  '@n8n/community-nodes/icon-prefer-themed-variants': 'warn',
97
99
  '@n8n/community-nodes/credential-documentation-url': 'error',
98
- '@n8n/community-nodes/resource-operation-pattern': 'warn',
100
+ '@n8n/community-nodes/resource-operation-pattern': 'error',
99
101
  '@n8n/community-nodes/trigger-node-conventions': 'error',
100
102
  '@n8n/community-nodes/cred-class-field-icon-missing': 'error',
101
103
  '@n8n/community-nodes/cred-class-oauth2-naming': 'error',
@@ -31,6 +31,7 @@ import { NodeClassDescriptionIconMissingRule } from './node-class-description-ic
31
31
  import { NodeConnectionTypeLiteralRule } from './node-connection-type-literal.js';
32
32
  import { NodeRegistrationCompleteRule } from './node-registration-complete.js';
33
33
  import { NodeUsableAsToolRule } from './node-usable-as-tool.js';
34
+ import { OpenEndedNodeFieldsRule } from './open-ended-node-fields.js';
34
35
  import { PackageNameConventionRule } from './package-name-convention.js';
35
36
  import { RequireFilesArrayRule } from './require-files-array.js';
36
37
  import { RequireHomepageRule } from './require-homepage.js';
@@ -54,6 +55,7 @@ export const rules = {
54
55
  'no-deprecated-workflow-functions': NoDeprecatedWorkflowFunctionsRule,
55
56
  'no-emoji-in-options': NoEmojiInOptionsRule,
56
57
  'node-usable-as-tool': NodeUsableAsToolRule,
58
+ 'open-ended-node-fields': OpenEndedNodeFieldsRule,
57
59
  'package-name-convention': PackageNameConventionRule,
58
60
  'credential-test-required': CredentialTestRequiredRule,
59
61
  'credential-unnecessary-password': CredentialUnnecessaryPasswordRule,
@@ -0,0 +1,122 @@
1
+ import { Linter } from 'eslint';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { configs } from '../plugin.js';
5
+
6
+ const linter = new Linter();
7
+
8
+ function createNodeCode(property: string): string {
9
+ return `
10
+ export class TestNode extends Node {
11
+ description = {
12
+ displayName: 'Test Node',
13
+ name: 'testNode',
14
+ group: ['transform'],
15
+ version: 1,
16
+ inputs: ['main'],
17
+ outputs: ['main'],
18
+ properties: [${property}],
19
+ };
20
+ }`;
21
+ }
22
+
23
+ function lintNode(code: string, config: keyof typeof configs) {
24
+ return linter.verify(code, [{ ...configs[config], files: ['**/*.node.ts'] }], {
25
+ filename: 'src/rules/TestNode.node.ts',
26
+ });
27
+ }
28
+
29
+ describe.each(['recommended', 'recommendedWithoutN8nCloudSupport'] as const)(
30
+ '%s node property review',
31
+ (config) => {
32
+ // CE-1879: Warn when an optional JSON field replaces typed node parameters.
33
+ it('warns about an optional JSON configuration catch-all', () => {
34
+ const code = createNodeCode(`{
35
+ displayName: 'Configuration',
36
+ name: 'configuration',
37
+ type: 'json',
38
+ required: false,
39
+ default: '{}',
40
+ }`);
41
+
42
+ const warnings = lintNode(code, config).filter(
43
+ ({ severity, message }) => severity === 1 && /Additional (Options|Fields)/i.test(message),
44
+ );
45
+
46
+ expect(warnings).toEqual([
47
+ expect.objectContaining({
48
+ severity: 1,
49
+ message: expect.stringMatching(/Additional (Options|Fields)/i),
50
+ }),
51
+ ]);
52
+ });
53
+
54
+ it('does not warn about typed parameters inside Additional Fields', () => {
55
+ const code = createNodeCode(`{
56
+ displayName: 'Additional Fields',
57
+ name: 'additionalFields',
58
+ type: 'collection',
59
+ default: {},
60
+ options: [{ displayName: 'Limit', name: 'limit', type: 'number', default: 10 }],
61
+ }`);
62
+
63
+ const messages = lintNode(code, config);
64
+ expect(messages).toEqual(
65
+ expect.arrayContaining([
66
+ expect.objectContaining({
67
+ ruleId: '@n8n/community-nodes/require-node-description-fields',
68
+ }),
69
+ ]),
70
+ );
71
+
72
+ const warnings = messages.filter(
73
+ ({ severity, message }) => severity === 1 && /Additional (Options|Fields)/i.test(message),
74
+ );
75
+
76
+ expect(warnings).toEqual([]);
77
+ });
78
+
79
+ it('warns when an open-ended JSON field does not declare required', () => {
80
+ const code = createNodeCode(`{
81
+ displayName: 'Configuration',
82
+ name: 'configuration',
83
+ type: 'json',
84
+ default: '{}',
85
+ }`);
86
+
87
+ expect(
88
+ lintNode(code, config).filter(
89
+ ({ ruleId }) => ruleId === '@n8n/community-nodes/open-ended-node-fields',
90
+ ),
91
+ ).toHaveLength(1);
92
+ });
93
+
94
+ it('does not warn about required JSON fields', () => {
95
+ const code = createNodeCode(`{
96
+ displayName: 'Request Body',
97
+ name: 'body',
98
+ type: 'json',
99
+ required: true,
100
+ default: '{}',
101
+ }`);
102
+
103
+ expect(
104
+ lintNode(code, config).filter(
105
+ ({ ruleId }) => ruleId === '@n8n/community-nodes/open-ended-node-fields',
106
+ ),
107
+ ).toEqual([]);
108
+ });
109
+
110
+ it('does not inspect classes that are not nodes', () => {
111
+ const code = `class Helper {
112
+ description = { properties: [{ type: 'json', required: false }] };
113
+ }`;
114
+
115
+ expect(
116
+ lintNode(code, config).filter(
117
+ ({ ruleId }) => ruleId === '@n8n/community-nodes/open-ended-node-fields',
118
+ ),
119
+ ).toEqual([]);
120
+ });
121
+ },
122
+ );
@@ -0,0 +1,55 @@
1
+ import { AST_NODE_TYPES } from '@typescript-eslint/utils';
2
+
3
+ import {
4
+ createRule,
5
+ findArrayLiteralProperty,
6
+ findNodeDescriptionObject,
7
+ findObjectProperty,
8
+ getBooleanLiteralValue,
9
+ getStringLiteralValue,
10
+ isFileType,
11
+ isNodeTypeClass,
12
+ } from '../utils/index.js';
13
+
14
+ export const OpenEndedNodeFieldsRule = createRule({
15
+ name: 'open-ended-node-fields',
16
+ meta: {
17
+ type: 'suggestion',
18
+ docs: {
19
+ description: 'Discourage optional open-ended JSON fields in node properties',
20
+ },
21
+ messages: {
22
+ optionalJson:
23
+ 'Optional JSON fields can hide configuration. Use typed parameters under Additional Options or Additional Fields instead.',
24
+ },
25
+ schema: [],
26
+ },
27
+ defaultOptions: [],
28
+ create(context) {
29
+ if (!isFileType(context.filename, '.node.ts')) return {};
30
+
31
+ return {
32
+ ClassDeclaration(node) {
33
+ if (!isNodeTypeClass(node)) return;
34
+
35
+ const description = findNodeDescriptionObject(node);
36
+ if (!description) return;
37
+
38
+ const properties = findArrayLiteralProperty(description, 'properties');
39
+ if (!properties) return;
40
+
41
+ for (const property of properties.elements) {
42
+ if (property?.type !== AST_NODE_TYPES.ObjectExpression) continue;
43
+
44
+ const type = findObjectProperty(property, 'type');
45
+ if (getStringLiteralValue(type?.value ?? null) !== 'json') continue;
46
+
47
+ const required = findObjectProperty(property, 'required');
48
+ if (getBooleanLiteralValue(required?.value ?? null) === true) continue;
49
+
50
+ context.report({ node: property, messageId: 'optionalJson' });
51
+ }
52
+ },
53
+ };
54
+ },
55
+ });
@@ -1,5 +1,8 @@
1
1
  import { RuleTester } from '@typescript-eslint/rule-tester';
2
+ import { Linter } from 'eslint';
3
+ import { describe, expect, it } from 'vitest';
2
4
 
5
+ import { configs } from '../plugin.js';
3
6
  import { ResourceOperationPatternRule } from './resource-operation-pattern.js';
4
7
 
5
8
  const ruleTester = new RuleTester();
@@ -215,3 +218,61 @@ ruleTester.run('resource-operation-pattern', ResourceOperationPatternRule, {
215
218
  },
216
219
  ],
217
220
  });
221
+
222
+ const resourceOperationRuleId = '@n8n/community-nodes/resource-operation-pattern';
223
+
224
+ function nodeWithSixOperations(hasResource: boolean): string {
225
+ return `
226
+ class TestNode extends Node {
227
+ description = {
228
+ properties: [
229
+ ${hasResource ? "{ name: 'resource', type: 'options', options: [{ name: 'User', value: 'user' }] }," : ''}
230
+ {
231
+ name: 'operation',
232
+ type: 'options',
233
+ options: [
234
+ { name: 'Get', value: 'get' },
235
+ { name: 'Create', value: 'create' },
236
+ { name: 'Update', value: 'update' },
237
+ { name: 'Delete', value: 'delete' },
238
+ { name: 'List', value: 'list' },
239
+ { name: 'Search', value: 'search' },
240
+ ],
241
+ },
242
+ ],
243
+ };
244
+ }
245
+ `;
246
+ }
247
+
248
+ describe.each([
249
+ ['recommended', configs.recommended],
250
+ ['recommendedWithoutN8nCloudSupport', configs.recommendedWithoutN8nCloudSupport],
251
+ ] as const)('%s config', (_name, config) => {
252
+ const linter = new Linter();
253
+ const lint = (code: string) =>
254
+ linter.verify(
255
+ code,
256
+ {
257
+ files: ['**/*.node.ts'],
258
+ plugins: config.plugins,
259
+ rules: { [resourceOperationRuleId]: config.rules[resourceOperationRuleId] },
260
+ },
261
+ { filename: 'TestNode.node.ts' },
262
+ );
263
+
264
+ // CE-3284: The resource finding must fail lint even when it is the only finding.
265
+ it('reports missing resources as the only error', () => {
266
+ expect(lint(nodeWithSixOperations(false))).toEqual([
267
+ expect.objectContaining({
268
+ ruleId: resourceOperationRuleId,
269
+ messageId: 'tooManyOperationsWithoutResources',
270
+ severity: 2,
271
+ }),
272
+ ]);
273
+ });
274
+
275
+ it('accepts operations organized by resource', () => {
276
+ expect(lint(nodeWithSixOperations(true))).toEqual([]);
277
+ });
278
+ });