@n8n/eslint-plugin-community-nodes 0.2.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.
Files changed (92) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/LICENSE.md +88 -0
  3. package/LICENSE_EE.md +27 -0
  4. package/dist/plugin.d.ts +61 -0
  5. package/dist/plugin.d.ts.map +1 -0
  6. package/dist/plugin.js +51 -0
  7. package/dist/plugin.js.map +1 -0
  8. package/dist/rules/credential-password-field.d.ts +3 -0
  9. package/dist/rules/credential-password-field.d.ts.map +1 -0
  10. package/dist/rules/credential-password-field.js +97 -0
  11. package/dist/rules/credential-password-field.js.map +1 -0
  12. package/dist/rules/credential-test-required.d.ts +3 -0
  13. package/dist/rules/credential-test-required.d.ts.map +1 -0
  14. package/dist/rules/credential-test-required.js +79 -0
  15. package/dist/rules/credential-test-required.js.map +1 -0
  16. package/dist/rules/icon-validation.d.ts +3 -0
  17. package/dist/rules/icon-validation.d.ts.map +1 -0
  18. package/dist/rules/icon-validation.js +131 -0
  19. package/dist/rules/icon-validation.js.map +1 -0
  20. package/dist/rules/index.d.ts +13 -0
  21. package/dist/rules/index.d.ts.map +1 -0
  22. package/dist/rules/index.js +23 -0
  23. package/dist/rules/index.js.map +1 -0
  24. package/dist/rules/no-credential-reuse.d.ts +3 -0
  25. package/dist/rules/no-credential-reuse.d.ts.map +1 -0
  26. package/dist/rules/no-credential-reuse.js +62 -0
  27. package/dist/rules/no-credential-reuse.js.map +1 -0
  28. package/dist/rules/no-deprecated-workflow-functions.d.ts +3 -0
  29. package/dist/rules/no-deprecated-workflow-functions.d.ts.map +1 -0
  30. package/dist/rules/no-deprecated-workflow-functions.js +144 -0
  31. package/dist/rules/no-deprecated-workflow-functions.js.map +1 -0
  32. package/dist/rules/no-restricted-globals.d.ts +3 -0
  33. package/dist/rules/no-restricted-globals.d.ts.map +1 -0
  34. package/dist/rules/no-restricted-globals.js +58 -0
  35. package/dist/rules/no-restricted-globals.js.map +1 -0
  36. package/dist/rules/no-restricted-imports.d.ts +3 -0
  37. package/dist/rules/no-restricted-imports.d.ts.map +1 -0
  38. package/dist/rules/no-restricted-imports.js +80 -0
  39. package/dist/rules/no-restricted-imports.js.map +1 -0
  40. package/dist/rules/node-usable-as-tool.d.ts +3 -0
  41. package/dist/rules/node-usable-as-tool.d.ts.map +1 -0
  42. package/dist/rules/node-usable-as-tool.js +57 -0
  43. package/dist/rules/node-usable-as-tool.js.map +1 -0
  44. package/dist/rules/package-name-convention.d.ts +3 -0
  45. package/dist/rules/package-name-convention.d.ts.map +1 -0
  46. package/dist/rules/package-name-convention.js +52 -0
  47. package/dist/rules/package-name-convention.js.map +1 -0
  48. package/dist/rules/resource-operation-pattern.d.ts +3 -0
  49. package/dist/rules/resource-operation-pattern.d.ts.map +1 -0
  50. package/dist/rules/resource-operation-pattern.js +77 -0
  51. package/dist/rules/resource-operation-pattern.js.map +1 -0
  52. package/dist/utils/ast-utils.d.ts +25 -0
  53. package/dist/utils/ast-utils.d.ts.map +1 -0
  54. package/dist/utils/ast-utils.js +117 -0
  55. package/dist/utils/ast-utils.js.map +1 -0
  56. package/dist/utils/file-utils.d.ts +12 -0
  57. package/dist/utils/file-utils.d.ts.map +1 -0
  58. package/dist/utils/file-utils.js +154 -0
  59. package/dist/utils/file-utils.js.map +1 -0
  60. package/dist/utils/index.d.ts +3 -0
  61. package/dist/utils/index.d.ts.map +1 -0
  62. package/dist/utils/index.js +3 -0
  63. package/dist/utils/index.js.map +1 -0
  64. package/package.json +46 -0
  65. package/src/plugin.ts +55 -0
  66. package/src/rules/credential-password-field.test.ts +231 -0
  67. package/src/rules/credential-password-field.ts +123 -0
  68. package/src/rules/credential-test-required.test.ts +147 -0
  69. package/src/rules/credential-test-required.ts +99 -0
  70. package/src/rules/icon-validation.test.ts +196 -0
  71. package/src/rules/icon-validation.ts +158 -0
  72. package/src/rules/index.ts +24 -0
  73. package/src/rules/no-credential-reuse.test.ts +226 -0
  74. package/src/rules/no-credential-reuse.ts +81 -0
  75. package/src/rules/no-deprecated-workflow-functions.test.ts +117 -0
  76. package/src/rules/no-deprecated-workflow-functions.ts +166 -0
  77. package/src/rules/no-restricted-globals.test.ts +135 -0
  78. package/src/rules/no-restricted-globals.ts +71 -0
  79. package/src/rules/no-restricted-imports.test.ts +181 -0
  80. package/src/rules/no-restricted-imports.ts +86 -0
  81. package/src/rules/node-usable-as-tool.test.ts +80 -0
  82. package/src/rules/node-usable-as-tool.ts +70 -0
  83. package/src/rules/package-name-convention.test.ts +112 -0
  84. package/src/rules/package-name-convention.ts +63 -0
  85. package/src/rules/resource-operation-pattern.test.ts +216 -0
  86. package/src/rules/resource-operation-pattern.ts +97 -0
  87. package/src/utils/ast-utils.ts +179 -0
  88. package/src/utils/file-utils.ts +204 -0
  89. package/src/utils/index.ts +2 -0
  90. package/tsconfig.json +11 -0
  91. package/tsconfig.tsbuildinfo +1 -0
  92. package/vite.config.ts +4 -0
@@ -0,0 +1,86 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ import { getModulePath, isDirectRequireCall, isRequireMemberCall } from '../utils/index.js';
3
+
4
+ const allowedModules = [
5
+ 'n8n-workflow',
6
+ 'lodash',
7
+ 'moment',
8
+ 'p-limit',
9
+ 'luxon',
10
+ 'zod',
11
+ 'crypto',
12
+ 'node:crypto',
13
+ ];
14
+
15
+ const isModuleAllowed = (modulePath: string): boolean => {
16
+ if (modulePath.startsWith('./') || modulePath.startsWith('../')) return true;
17
+
18
+ const moduleName = modulePath.startsWith('@')
19
+ ? modulePath.split('/').slice(0, 2).join('/')
20
+ : modulePath.split('/')[0];
21
+ if (!moduleName) return true;
22
+ return allowedModules.includes(moduleName);
23
+ };
24
+
25
+ export const NoRestrictedImportsRule = ESLintUtils.RuleCreator.withoutDocs({
26
+ meta: {
27
+ type: 'problem',
28
+ docs: {
29
+ description: 'Disallow usage of restricted imports in community nodes.',
30
+ },
31
+ messages: {
32
+ restrictedImport:
33
+ "Import of '{{ modulePath }}' is not allowed. n8n Cloud does not allow community nodes with dependencies.",
34
+ restrictedRequire:
35
+ "Require of '{{ modulePath }}' is not allowed. n8n Cloud does not allow community nodes with dependencies.",
36
+ restrictedDynamicImport:
37
+ "Dynamic import of '{{ modulePath }}' is not allowed. n8n Cloud does not allow community nodes with dependencies.",
38
+ },
39
+ schema: [],
40
+ },
41
+ defaultOptions: [],
42
+ create(context) {
43
+ return {
44
+ ImportDeclaration(node) {
45
+ const modulePath = getModulePath(node.source);
46
+ if (modulePath && !isModuleAllowed(modulePath)) {
47
+ context.report({
48
+ node,
49
+ messageId: 'restrictedImport',
50
+ data: {
51
+ modulePath,
52
+ },
53
+ });
54
+ }
55
+ },
56
+
57
+ ImportExpression(node) {
58
+ const modulePath = getModulePath(node.source);
59
+ if (modulePath && !isModuleAllowed(modulePath)) {
60
+ context.report({
61
+ node,
62
+ messageId: 'restrictedDynamicImport',
63
+ data: {
64
+ modulePath,
65
+ },
66
+ });
67
+ }
68
+ },
69
+
70
+ CallExpression(node) {
71
+ if (isDirectRequireCall(node) || isRequireMemberCall(node)) {
72
+ const modulePath = getModulePath(node.arguments[0] ?? null);
73
+ if (modulePath && !isModuleAllowed(modulePath)) {
74
+ context.report({
75
+ node,
76
+ messageId: 'restrictedRequire',
77
+ data: {
78
+ modulePath,
79
+ },
80
+ });
81
+ }
82
+ }
83
+ },
84
+ };
85
+ },
86
+ });
@@ -0,0 +1,80 @@
1
+ import { RuleTester } from '@typescript-eslint/rule-tester';
2
+ import { NodeUsableAsToolRule } from './node-usable-as-tool.js';
3
+
4
+ const ruleTester = new RuleTester();
5
+
6
+ function createNodeCode(
7
+ usableAsTool?: boolean | 'missing',
8
+ hasDescription: boolean = true,
9
+ ): string {
10
+ let usableAsToolProperty = '';
11
+ if (usableAsTool === true) {
12
+ usableAsToolProperty = ',\n\t\tusableAsTool: true';
13
+ } else if (usableAsTool === false) {
14
+ usableAsToolProperty = ',\n\t\tusableAsTool: false';
15
+ }
16
+
17
+ if (!hasDescription) {
18
+ return `
19
+ import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
20
+
21
+ export class TestNode implements INodeType {
22
+ displayName = 'Test Node';
23
+ }`;
24
+ }
25
+
26
+ return `
27
+ import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
28
+
29
+ export class TestNode implements INodeType {
30
+ description: INodeTypeDescription = {
31
+ displayName: 'Test Node',
32
+ name: 'testNode',
33
+ group: ['input'],
34
+ version: 1,
35
+ description: 'A test node',
36
+ defaults: {
37
+ name: 'Test Node',
38
+ },
39
+ inputs: ['main'],
40
+ outputs: ['main'],
41
+ properties: []${usableAsToolProperty},
42
+ };
43
+ }`;
44
+ }
45
+
46
+ function createNonNodeClass(): string {
47
+ return `
48
+ export class RegularClass {
49
+ someProperty = 'value';
50
+ }`;
51
+ }
52
+
53
+ ruleTester.run('node-usable-as-tool', NodeUsableAsToolRule, {
54
+ valid: [
55
+ {
56
+ name: 'node with usableAsTool set to true',
57
+ code: createNodeCode(true),
58
+ },
59
+ {
60
+ name: 'class that does not implement INodeType',
61
+ code: createNonNodeClass(),
62
+ },
63
+ {
64
+ name: 'node with usableAsTool set to false',
65
+ code: createNodeCode(false),
66
+ },
67
+ {
68
+ name: 'node without description property',
69
+ code: createNodeCode(undefined, false),
70
+ },
71
+ ],
72
+ invalid: [
73
+ {
74
+ name: 'node missing usableAsTool property',
75
+ code: createNodeCode('missing'),
76
+ errors: [{ messageId: 'missingUsableAsTool' }],
77
+ output: createNodeCode(true),
78
+ },
79
+ ],
80
+ });
@@ -0,0 +1,70 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ import {
3
+ isNodeTypeClass,
4
+ findClassProperty,
5
+ findObjectProperty,
6
+ getBooleanLiteralValue,
7
+ } from '../utils/index.js';
8
+
9
+ export const NodeUsableAsToolRule = ESLintUtils.RuleCreator.withoutDocs({
10
+ meta: {
11
+ type: 'problem',
12
+ docs: {
13
+ description: 'Ensure node classes have usableAsTool property',
14
+ },
15
+ messages: {
16
+ missingUsableAsTool:
17
+ 'Node class should have usableAsTool property. When in doubt, set it to true.',
18
+ },
19
+ fixable: 'code',
20
+ schema: [],
21
+ },
22
+ defaultOptions: [],
23
+ create(context) {
24
+ return {
25
+ ClassDeclaration(node) {
26
+ if (!isNodeTypeClass(node)) {
27
+ return;
28
+ }
29
+
30
+ const descriptionProperty = findClassProperty(node, 'description');
31
+ if (!descriptionProperty) {
32
+ return;
33
+ }
34
+
35
+ const descriptionValue = descriptionProperty.value;
36
+ if (descriptionValue?.type !== 'ObjectExpression') {
37
+ return;
38
+ }
39
+
40
+ const usableAsToolProperty = findObjectProperty(descriptionValue, 'usableAsTool');
41
+
42
+ if (!usableAsToolProperty) {
43
+ context.report({
44
+ node,
45
+ messageId: 'missingUsableAsTool',
46
+ fix(fixer) {
47
+ if (descriptionValue?.type === 'ObjectExpression') {
48
+ const properties = descriptionValue.properties;
49
+ if (properties.length === 0) {
50
+ const openBrace = descriptionValue.range![0] + 1;
51
+ return fixer.insertTextAfterRange(
52
+ [openBrace, openBrace],
53
+ '\n\t\tusableAsTool: true,',
54
+ );
55
+ } else {
56
+ const lastProperty = properties.at(-1);
57
+ if (lastProperty) {
58
+ return fixer.insertTextAfter(lastProperty, ',\n\t\tusableAsTool: true');
59
+ }
60
+ }
61
+ }
62
+
63
+ return null;
64
+ },
65
+ });
66
+ }
67
+ },
68
+ };
69
+ },
70
+ });
@@ -0,0 +1,112 @@
1
+ import { RuleTester } from '@typescript-eslint/rule-tester';
2
+ import { PackageNameConventionRule } from './package-name-convention.js';
3
+
4
+ const ruleTester = new RuleTester();
5
+
6
+ ruleTester.run('package-name-convention', PackageNameConventionRule, {
7
+ valid: [
8
+ {
9
+ name: 'valid unscoped package name',
10
+ filename: 'package.json',
11
+ code: '{ "name": "n8n-nodes-example", "version": "1.0.0" }',
12
+ },
13
+ {
14
+ name: 'valid unscoped package name with dashes',
15
+ filename: 'package.json',
16
+ code: '{ "name": "n8n-nodes-my-service", "version": "1.0.0" }',
17
+ },
18
+ {
19
+ name: 'valid scoped package name',
20
+ filename: 'package.json',
21
+ code: '{ "name": "@mycompany/n8n-nodes-example", "version": "1.0.0" }',
22
+ },
23
+ {
24
+ name: 'valid scoped package name with dashes',
25
+ filename: 'package.json',
26
+ code: '{ "name": "@author/n8n-nodes-service", "version": "1.0.0" }',
27
+ },
28
+ {
29
+ name: 'object without name property',
30
+ filename: 'package.json',
31
+ code: '{ "version": "1.0.0", "description": "test" }',
32
+ },
33
+ {
34
+ name: 'non-package.json file ignored',
35
+ filename: 'some-config.json',
36
+ code: '{ "name": "my-config", "type": "config" }',
37
+ },
38
+ {
39
+ name: 'nested name fields should be ignored - only top-level name matters',
40
+ filename: 'package.json',
41
+ code: `{
42
+ "name": "n8n-nodes-example",
43
+ "version": "1.0.0",
44
+ "dependencies": {
45
+ "name": "invalid-nested-name"
46
+ },
47
+ "scripts": {
48
+ "name": "another-invalid-name"
49
+ },
50
+ "author": {
51
+ "name": "John Doe"
52
+ }
53
+ }`,
54
+ },
55
+ {
56
+ name: 'deeply nested name fields should be ignored',
57
+ filename: 'package.json',
58
+ code: `{
59
+ "name": "@author/n8n-nodes-service",
60
+ "version": "1.0.0",
61
+ "config": {
62
+ "nested": {
63
+ "deeply": {
64
+ "name": "very-invalid-name"
65
+ }
66
+ }
67
+ },
68
+ "repository": {
69
+ "type": "git",
70
+ "url": "https://github.com/user/repo",
71
+ "directory": {
72
+ "name": "bad-name"
73
+ }
74
+ }
75
+ }`,
76
+ },
77
+ ],
78
+ invalid: [
79
+ {
80
+ name: 'invalid package name - generic',
81
+ filename: 'package.json',
82
+ code: '{ "name": "my-package", "version": "1.0.0" }',
83
+ errors: [{ messageId: 'invalidPackageName', data: { packageName: 'my-package' } }],
84
+ },
85
+ {
86
+ name: 'invalid package name - missing nodes',
87
+ filename: 'package.json',
88
+ code: '{ "name": "n8n-example", "version": "1.0.0" }',
89
+ errors: [{ messageId: 'invalidPackageName', data: { packageName: 'n8n-example' } }],
90
+ },
91
+ {
92
+ name: 'invalid scoped package name',
93
+ filename: 'package.json',
94
+ code: '{ "name": "@company/example-nodes", "version": "1.0.0" }',
95
+ errors: [
96
+ { messageId: 'invalidPackageName', data: { packageName: '@company/example-nodes' } },
97
+ ],
98
+ },
99
+ {
100
+ name: 'invalid package name - wrong order',
101
+ filename: 'package.json',
102
+ code: '{ "name": "nodes-n8n-example", "version": "1.0.0" }',
103
+ errors: [{ messageId: 'invalidPackageName', data: { packageName: 'nodes-n8n-example' } }],
104
+ },
105
+ {
106
+ name: 'empty package name',
107
+ filename: 'package.json',
108
+ code: '{ "name": "", "version": "1.0.0" }',
109
+ errors: [{ messageId: 'invalidPackageName', data: { packageName: '' } }],
110
+ },
111
+ ],
112
+ });
@@ -0,0 +1,63 @@
1
+ import { ESLintUtils, TSESTree, AST_NODE_TYPES } from '@typescript-eslint/utils';
2
+
3
+ export const PackageNameConventionRule = ESLintUtils.RuleCreator.withoutDocs({
4
+ meta: {
5
+ type: 'problem',
6
+ docs: {
7
+ description: 'Enforce correct package naming convention for n8n community nodes',
8
+ },
9
+ messages: {
10
+ invalidPackageName:
11
+ 'Package name "{{ packageName }}" must follow the convention "n8n-nodes-[PACKAGE-NAME]" or "@[AUTHOR]/n8n-nodes-[PACKAGE-NAME]"',
12
+ },
13
+ schema: [],
14
+ },
15
+ defaultOptions: [],
16
+ create(context) {
17
+ if (!context.filename.endsWith('package.json')) {
18
+ return {};
19
+ }
20
+
21
+ return {
22
+ ObjectExpression(node: TSESTree.ObjectExpression) {
23
+ if (node.parent?.type === AST_NODE_TYPES.Property) {
24
+ return;
25
+ }
26
+
27
+ const nameProperty = node.properties.find(
28
+ (property) =>
29
+ property.type === AST_NODE_TYPES.Property &&
30
+ property.key.type === AST_NODE_TYPES.Literal &&
31
+ property.key.value === 'name',
32
+ );
33
+
34
+ if (!nameProperty || nameProperty.type !== AST_NODE_TYPES.Property) {
35
+ return;
36
+ }
37
+
38
+ if (nameProperty.value.type !== AST_NODE_TYPES.Literal) {
39
+ return;
40
+ }
41
+
42
+ const packageName = nameProperty.value.value;
43
+ const packageNameStr = typeof packageName === 'string' ? packageName : null;
44
+
45
+ if (!packageNameStr || !isValidPackageName(packageNameStr)) {
46
+ context.report({
47
+ node: nameProperty,
48
+ messageId: 'invalidPackageName',
49
+ data: {
50
+ packageName: packageNameStr ?? 'undefined',
51
+ },
52
+ });
53
+ }
54
+ },
55
+ };
56
+ },
57
+ });
58
+
59
+ function isValidPackageName(name: string): boolean {
60
+ const unscoped = /^n8n-nodes-.+$/;
61
+ const scoped = /^@.+\/n8n-nodes-.+$/;
62
+ return unscoped.test(name) || scoped.test(name);
63
+ }
@@ -0,0 +1,216 @@
1
+ import { RuleTester } from '@typescript-eslint/rule-tester';
2
+ import { ResourceOperationPatternRule } from './resource-operation-pattern.js';
3
+
4
+ const ruleTester = new RuleTester();
5
+
6
+ ruleTester.run('resource-operation-pattern', ResourceOperationPatternRule, {
7
+ valid: [
8
+ {
9
+ name: 'node with resources and operations (good pattern)',
10
+ filename: '/tmp/TestNode.node.ts',
11
+ code: `
12
+ import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
13
+
14
+ export class TestNode implements INodeType {
15
+ description: INodeTypeDescription = {
16
+ displayName: 'Test Node',
17
+ name: 'testNode',
18
+ group: ['output'],
19
+ version: 1,
20
+ inputs: ['main'],
21
+ outputs: ['main'],
22
+ properties: [
23
+ {
24
+ displayName: 'Resource',
25
+ name: 'resource',
26
+ type: 'options',
27
+ options: [
28
+ { name: 'User', value: 'user' },
29
+ { name: 'Project', value: 'project' }
30
+ ],
31
+ default: 'user'
32
+ },
33
+ {
34
+ displayName: 'Operation',
35
+ name: 'operation',
36
+ type: 'options',
37
+ options: [
38
+ { name: 'Get', value: 'get' },
39
+ { name: 'Create', value: 'create' },
40
+ { name: 'Update', value: 'update' },
41
+ { name: 'Delete', value: 'delete' }
42
+ ],
43
+ default: 'get'
44
+ }
45
+ ]
46
+ };
47
+ }
48
+ `,
49
+ },
50
+ {
51
+ name: 'node without operations property',
52
+ filename: '/tmp/TestNode.node.ts',
53
+ code: `
54
+ import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
55
+
56
+ export class TestNode implements INodeType {
57
+ description: INodeTypeDescription = {
58
+ displayName: 'Test Node',
59
+ name: 'testNode',
60
+ group: ['output'],
61
+ version: 1,
62
+ inputs: ['main'],
63
+ outputs: ['main'],
64
+ properties: [
65
+ {
66
+ displayName: 'API Key',
67
+ name: 'apiKey',
68
+ type: 'string',
69
+ default: ''
70
+ }
71
+ ]
72
+ };
73
+ }
74
+ `,
75
+ },
76
+ {
77
+ name: 'non-node class ignored',
78
+ filename: '/tmp/TestNode.node.ts',
79
+ code: `
80
+ export class NotANode {
81
+ description = {
82
+ properties: [
83
+ {
84
+ displayName: 'Operation',
85
+ name: 'operation',
86
+ type: 'options',
87
+ options: [
88
+ { name: 'Get', value: 'get' },
89
+ { name: 'Create', value: 'create' }
90
+ ],
91
+ default: 'get'
92
+ }
93
+ ]
94
+ };
95
+ }
96
+ `,
97
+ },
98
+ {
99
+ name: 'node with exactly 5 operations without resources (allowed)',
100
+ filename: '/tmp/TestNode.node.ts',
101
+ code: `
102
+ import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
103
+
104
+ export class TestNode implements INodeType {
105
+ description: INodeTypeDescription = {
106
+ displayName: 'Test Node',
107
+ name: 'testNode',
108
+ group: ['output'],
109
+ version: 1,
110
+ inputs: ['main'],
111
+ outputs: ['main'],
112
+ properties: [
113
+ {
114
+ displayName: 'Operation',
115
+ name: 'operation',
116
+ type: 'options',
117
+ options: [
118
+ { name: 'Get', value: 'get' },
119
+ { name: 'Create', value: 'create' },
120
+ { name: 'Update', value: 'update' },
121
+ { name: 'Delete', value: 'delete' },
122
+ { name: 'List', value: 'list' }
123
+ ],
124
+ default: 'get'
125
+ }
126
+ ]
127
+ };
128
+ }
129
+ `,
130
+ },
131
+ ],
132
+ invalid: [
133
+ {
134
+ name: 'node with exactly 6 operations without resources (error)',
135
+ filename: '/tmp/TestNode.node.ts',
136
+ code: `
137
+ import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
138
+
139
+ export class TestNode implements INodeType {
140
+ description: INodeTypeDescription = {
141
+ displayName: 'Test Node',
142
+ name: 'testNode',
143
+ group: ['output'],
144
+ version: 1,
145
+ inputs: ['main'],
146
+ outputs: ['main'],
147
+ properties: [
148
+ {
149
+ displayName: 'Operation',
150
+ name: 'operation',
151
+ type: 'options',
152
+ options: [
153
+ { name: 'Get', value: 'get' },
154
+ { name: 'Create', value: 'create' },
155
+ { name: 'Update', value: 'update' },
156
+ { name: 'Delete', value: 'delete' },
157
+ { name: 'List', value: 'list' },
158
+ { name: 'Search', value: 'search' }
159
+ ],
160
+ default: 'get'
161
+ }
162
+ ]
163
+ };
164
+ }
165
+ `,
166
+ errors: [
167
+ {
168
+ messageId: 'tooManyOperationsWithoutResources',
169
+ data: { operationCount: '6' },
170
+ },
171
+ ],
172
+ },
173
+ {
174
+ name: 'node with many operations without resources (error)',
175
+ filename: '/tmp/TestNode.node.ts',
176
+ code: `
177
+ import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
178
+
179
+ export class TestNode implements INodeType {
180
+ description: INodeTypeDescription = {
181
+ displayName: 'Test Node',
182
+ name: 'testNode',
183
+ group: ['output'],
184
+ version: 1,
185
+ inputs: ['main'],
186
+ outputs: ['main'],
187
+ properties: [
188
+ {
189
+ displayName: 'Operation',
190
+ name: 'operation',
191
+ type: 'options',
192
+ options: [
193
+ { name: 'Get User', value: 'getUser' },
194
+ { name: 'Create User', value: 'createUser' },
195
+ { name: 'Update User', value: 'updateUser' },
196
+ { name: 'Delete User', value: 'deleteUser' },
197
+ { name: 'List Users', value: 'listUsers' },
198
+ { name: 'Get Project', value: 'getProject' },
199
+ { name: 'Create Project', value: 'createProject' },
200
+ { name: 'Update Project', value: 'updateProject' }
201
+ ],
202
+ default: 'getUser'
203
+ }
204
+ ]
205
+ };
206
+ }
207
+ `,
208
+ errors: [
209
+ {
210
+ messageId: 'tooManyOperationsWithoutResources',
211
+ data: { operationCount: '8' },
212
+ },
213
+ ],
214
+ },
215
+ ],
216
+ });