@n8n/eslint-plugin-community-nodes 0.10.0 → 0.12.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/.turbo/turbo-build.log +1 -1
- package/README.md +22 -17
- package/dist/plugin.d.ts +30 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +10 -0
- package/dist/plugin.js.map +1 -1
- package/dist/rules/index.d.ts +5 -0
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +10 -0
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/missing-paired-item.d.ts +16 -0
- package/dist/rules/missing-paired-item.d.ts.map +1 -0
- package/dist/rules/missing-paired-item.js +121 -0
- package/dist/rules/missing-paired-item.js.map +1 -0
- package/dist/rules/no-forbidden-lifecycle-scripts.d.ts +2 -0
- package/dist/rules/no-forbidden-lifecycle-scripts.d.ts.map +1 -0
- package/dist/rules/no-forbidden-lifecycle-scripts.js +59 -0
- package/dist/rules/no-forbidden-lifecycle-scripts.js.map +1 -0
- package/dist/rules/node-connection-type-literal.d.ts +2 -0
- package/dist/rules/node-connection-type-literal.d.ts.map +1 -0
- package/dist/rules/node-connection-type-literal.js +77 -0
- package/dist/rules/node-connection-type-literal.js.map +1 -0
- package/dist/rules/options-sorted-alphabetically.d.ts +2 -0
- package/dist/rules/options-sorted-alphabetically.d.ts.map +1 -0
- package/dist/rules/options-sorted-alphabetically.js +95 -0
- package/dist/rules/options-sorted-alphabetically.js.map +1 -0
- package/dist/rules/require-continue-on-fail.d.ts +2 -0
- package/dist/rules/require-continue-on-fail.d.ts.map +1 -0
- package/dist/rules/require-continue-on-fail.js +76 -0
- package/dist/rules/require-continue-on-fail.js.map +1 -0
- package/docs/rules/missing-paired-item.md +70 -0
- package/docs/rules/no-forbidden-lifecycle-scripts.md +46 -0
- package/docs/rules/node-connection-type-literal.md +46 -0
- package/docs/rules/options-sorted-alphabetically.md +63 -0
- package/docs/rules/require-continue-on-fail.md +56 -0
- package/package.json +9 -6
- package/src/plugin.ts +10 -0
- package/src/rules/index.ts +10 -0
- package/src/rules/missing-paired-item.test.ts +229 -0
- package/src/rules/missing-paired-item.ts +149 -0
- package/src/rules/no-forbidden-lifecycle-scripts.test.ts +103 -0
- package/src/rules/no-forbidden-lifecycle-scripts.ts +69 -0
- package/src/rules/node-connection-type-literal.test.ts +128 -0
- package/src/rules/node-connection-type-literal.ts +98 -0
- package/src/rules/options-sorted-alphabetically.test.ts +468 -0
- package/src/rules/options-sorted-alphabetically.ts +129 -0
- package/src/rules/require-continue-on-fail.test.ts +129 -0
- package/src/rules/require-continue-on-fail.ts +88 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +1 -1
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
|
|
3
|
+
import { NodeConnectionTypeLiteralRule } from './node-connection-type-literal.js';
|
|
4
|
+
|
|
5
|
+
const ruleTester = new RuleTester();
|
|
6
|
+
|
|
7
|
+
function createNodeCode(inputs: string, outputs: string): string {
|
|
8
|
+
return `
|
|
9
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
10
|
+
import { NodeConnectionTypes } from 'n8n-workflow';
|
|
11
|
+
|
|
12
|
+
export class TestNode implements INodeType {
|
|
13
|
+
description: INodeTypeDescription = {
|
|
14
|
+
displayName: 'Test Node',
|
|
15
|
+
name: 'testNode',
|
|
16
|
+
group: ['input'],
|
|
17
|
+
version: 1,
|
|
18
|
+
description: 'A test node',
|
|
19
|
+
defaults: { name: 'Test Node' },
|
|
20
|
+
inputs: ${inputs},
|
|
21
|
+
outputs: ${outputs},
|
|
22
|
+
properties: [],
|
|
23
|
+
};
|
|
24
|
+
}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function createNodeCodeNoImport(inputs: string, outputs: string): string {
|
|
28
|
+
return `
|
|
29
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
30
|
+
|
|
31
|
+
export class TestNode implements INodeType {
|
|
32
|
+
description: INodeTypeDescription = {
|
|
33
|
+
displayName: 'Test Node',
|
|
34
|
+
name: 'testNode',
|
|
35
|
+
group: ['input'],
|
|
36
|
+
version: 1,
|
|
37
|
+
description: 'A test node',
|
|
38
|
+
defaults: { name: 'Test Node' },
|
|
39
|
+
inputs: ${inputs},
|
|
40
|
+
outputs: ${outputs},
|
|
41
|
+
properties: [],
|
|
42
|
+
};
|
|
43
|
+
}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function createNonNodeClass(): string {
|
|
47
|
+
return `
|
|
48
|
+
export class RegularClass {
|
|
49
|
+
someProperty = 'value';
|
|
50
|
+
}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
ruleTester.run('node-connection-type-literal', NodeConnectionTypeLiteralRule, {
|
|
54
|
+
valid: [
|
|
55
|
+
{
|
|
56
|
+
name: 'class that does not implement INodeType',
|
|
57
|
+
code: createNonNodeClass(),
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'node with enum in inputs and outputs',
|
|
61
|
+
code: createNodeCode('[NodeConnectionTypes.Main]', '[NodeConnectionTypes.Main]'),
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'node with empty inputs and outputs',
|
|
65
|
+
code: createNodeCode('[]', '[]'),
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'node with AI enum in inputs',
|
|
69
|
+
code: createNodeCode('[NodeConnectionTypes.AiAgent]', '[NodeConnectionTypes.Main]'),
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'node with multiple enum values',
|
|
73
|
+
code: createNodeCode(
|
|
74
|
+
'[NodeConnectionTypes.Main]',
|
|
75
|
+
'[NodeConnectionTypes.AiAgent, NodeConnectionTypes.AiTool]',
|
|
76
|
+
),
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
invalid: [
|
|
80
|
+
{
|
|
81
|
+
name: 'string literal "main" in inputs',
|
|
82
|
+
code: createNodeCodeNoImport("['main']", '[NodeConnectionTypes.Main]'),
|
|
83
|
+
errors: [{ messageId: 'stringLiteralInInputs', data: { value: 'main', enumKey: 'Main' } }],
|
|
84
|
+
output: createNodeCodeNoImport('[NodeConnectionTypes.Main]', '[NodeConnectionTypes.Main]'),
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'string literal "main" in outputs',
|
|
88
|
+
code: createNodeCodeNoImport('[NodeConnectionTypes.Main]', "['main']"),
|
|
89
|
+
errors: [{ messageId: 'stringLiteralInOutputs', data: { value: 'main', enumKey: 'Main' } }],
|
|
90
|
+
output: createNodeCodeNoImport('[NodeConnectionTypes.Main]', '[NodeConnectionTypes.Main]'),
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'string literals in both inputs and outputs',
|
|
94
|
+
code: createNodeCodeNoImport("['main']", "['main']"),
|
|
95
|
+
errors: [
|
|
96
|
+
{ messageId: 'stringLiteralInInputs', data: { value: 'main', enumKey: 'Main' } },
|
|
97
|
+
{ messageId: 'stringLiteralInOutputs', data: { value: 'main', enumKey: 'Main' } },
|
|
98
|
+
],
|
|
99
|
+
output: createNodeCodeNoImport('[NodeConnectionTypes.Main]', '[NodeConnectionTypes.Main]'),
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'string literal "ai_agent" in inputs',
|
|
103
|
+
code: createNodeCodeNoImport("['ai_agent']", '[]'),
|
|
104
|
+
errors: [
|
|
105
|
+
{ messageId: 'stringLiteralInInputs', data: { value: 'ai_agent', enumKey: 'AiAgent' } },
|
|
106
|
+
],
|
|
107
|
+
output: createNodeCodeNoImport('[NodeConnectionTypes.AiAgent]', '[]'),
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'unknown string literal in inputs — no autofix',
|
|
111
|
+
code: createNodeCodeNoImport("['unknown_type']", '[]'),
|
|
112
|
+
errors: [{ messageId: 'unknownStringLiteralInInputs', data: { value: 'unknown_type' } }],
|
|
113
|
+
output: null,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: 'unknown string literal in outputs — no autofix',
|
|
117
|
+
code: createNodeCodeNoImport('[]', "['unknown_type']"),
|
|
118
|
+
errors: [{ messageId: 'unknownStringLiteralInOutputs', data: { value: 'unknown_type' } }],
|
|
119
|
+
output: null,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'string literal in node that already imports NodeConnectionTypes',
|
|
123
|
+
code: createNodeCode("['main']", '[NodeConnectionTypes.Main]'),
|
|
124
|
+
errors: [{ messageId: 'stringLiteralInInputs', data: { value: 'main', enumKey: 'Main' } }],
|
|
125
|
+
output: createNodeCode('[NodeConnectionTypes.Main]', '[NodeConnectionTypes.Main]'),
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
2
|
+
import type { TSESTree } from '@typescript-eslint/utils';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
isNodeTypeClass,
|
|
7
|
+
findClassProperty,
|
|
8
|
+
findObjectProperty,
|
|
9
|
+
createRule,
|
|
10
|
+
} from '../utils/index.js';
|
|
11
|
+
|
|
12
|
+
// n8n-workflow's ESM dist uses bare module specifiers that Node's native ESM
|
|
13
|
+
// loader cannot resolve. Loading via CJS (createRequire) sidesteps this.
|
|
14
|
+
const { NodeConnectionTypes } = createRequire(import.meta.url)('n8n-workflow') as {
|
|
15
|
+
NodeConnectionTypes: Record<string, string>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Reverse map: string value (e.g. 'main') → enum key name (e.g. 'Main').
|
|
19
|
+
// Derived directly from NodeConnectionTypes so it stays in sync automatically.
|
|
20
|
+
const VALUE_TO_ENUM_KEY: Record<string, string> = Object.fromEntries(
|
|
21
|
+
Object.entries(NodeConnectionTypes).map(([key, value]) => [value, key]),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export const NodeConnectionTypeLiteralRule = createRule({
|
|
25
|
+
name: 'node-connection-type-literal',
|
|
26
|
+
meta: {
|
|
27
|
+
type: 'problem',
|
|
28
|
+
docs: {
|
|
29
|
+
description:
|
|
30
|
+
'Disallow string literals in node description `inputs`/`outputs` — use `NodeConnectionTypes` enum instead',
|
|
31
|
+
},
|
|
32
|
+
messages: {
|
|
33
|
+
stringLiteralInInputs:
|
|
34
|
+
'Use NodeConnectionTypes.{{enumKey}} from "n8n-workflow" instead of the string literal "{{value}}" in "inputs".',
|
|
35
|
+
stringLiteralInOutputs:
|
|
36
|
+
'Use NodeConnectionTypes.{{enumKey}} from "n8n-workflow" instead of the string literal "{{value}}" in "outputs".',
|
|
37
|
+
unknownStringLiteralInInputs:
|
|
38
|
+
'Use the NodeConnectionTypes enum from "n8n-workflow" instead of the string literal "{{value}}" in "inputs".',
|
|
39
|
+
unknownStringLiteralInOutputs:
|
|
40
|
+
'Use the NodeConnectionTypes enum from "n8n-workflow" instead of the string literal "{{value}}" in "outputs".',
|
|
41
|
+
},
|
|
42
|
+
fixable: 'code',
|
|
43
|
+
schema: [],
|
|
44
|
+
},
|
|
45
|
+
defaultOptions: [],
|
|
46
|
+
create(context) {
|
|
47
|
+
function checkArrayElements(
|
|
48
|
+
elements: TSESTree.ArrayExpression['elements'],
|
|
49
|
+
property: 'inputs' | 'outputs',
|
|
50
|
+
) {
|
|
51
|
+
for (const element of elements) {
|
|
52
|
+
if (element?.type !== AST_NODE_TYPES.Literal) continue;
|
|
53
|
+
if (typeof element.value !== 'string') continue;
|
|
54
|
+
|
|
55
|
+
const value = element.value;
|
|
56
|
+
const enumKey = VALUE_TO_ENUM_KEY[value];
|
|
57
|
+
|
|
58
|
+
if (enumKey) {
|
|
59
|
+
context.report({
|
|
60
|
+
node: element,
|
|
61
|
+
messageId: property === 'inputs' ? 'stringLiteralInInputs' : 'stringLiteralInOutputs',
|
|
62
|
+
data: { value, enumKey },
|
|
63
|
+
fix(fixer) {
|
|
64
|
+
return fixer.replaceText(element, `NodeConnectionTypes.${enumKey}`);
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
} else {
|
|
68
|
+
context.report({
|
|
69
|
+
node: element,
|
|
70
|
+
messageId:
|
|
71
|
+
property === 'inputs'
|
|
72
|
+
? 'unknownStringLiteralInInputs'
|
|
73
|
+
: 'unknownStringLiteralInOutputs',
|
|
74
|
+
data: { value },
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
ClassDeclaration(node) {
|
|
82
|
+
if (!isNodeTypeClass(node)) return;
|
|
83
|
+
|
|
84
|
+
const descriptionProperty = findClassProperty(node, 'description');
|
|
85
|
+
if (!descriptionProperty) return;
|
|
86
|
+
|
|
87
|
+
const descriptionValue = descriptionProperty.value;
|
|
88
|
+
if (descriptionValue?.type !== AST_NODE_TYPES.ObjectExpression) return;
|
|
89
|
+
|
|
90
|
+
for (const prop of ['inputs', 'outputs'] as const) {
|
|
91
|
+
const property = findObjectProperty(descriptionValue, prop);
|
|
92
|
+
if (property?.value.type !== AST_NODE_TYPES.ArrayExpression) continue;
|
|
93
|
+
checkArrayElements(property.value.elements, prop);
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
});
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
|
|
3
|
+
import { OptionsSortedAlphabeticallyRule } from './options-sorted-alphabetically.js';
|
|
4
|
+
|
|
5
|
+
const ruleTester = new RuleTester();
|
|
6
|
+
|
|
7
|
+
ruleTester.run('options-sorted-alphabetically', OptionsSortedAlphabeticallyRule, {
|
|
8
|
+
valid: [
|
|
9
|
+
{
|
|
10
|
+
name: 'resource options already sorted alphabetically',
|
|
11
|
+
filename: '/tmp/TestNode.node.ts',
|
|
12
|
+
code: `
|
|
13
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
14
|
+
|
|
15
|
+
export class TestNode implements INodeType {
|
|
16
|
+
description: INodeTypeDescription = {
|
|
17
|
+
displayName: 'Test Node',
|
|
18
|
+
name: 'testNode',
|
|
19
|
+
properties: [
|
|
20
|
+
{
|
|
21
|
+
displayName: 'Resource',
|
|
22
|
+
name: 'resource',
|
|
23
|
+
type: 'options',
|
|
24
|
+
options: [
|
|
25
|
+
{ name: 'Contact', value: 'contact' },
|
|
26
|
+
{ name: 'Project', value: 'project' },
|
|
27
|
+
{ name: 'User', value: 'user' },
|
|
28
|
+
],
|
|
29
|
+
default: 'contact',
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
`,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'operation options already sorted alphabetically',
|
|
38
|
+
filename: '/tmp/TestNode.node.ts',
|
|
39
|
+
code: `
|
|
40
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
41
|
+
|
|
42
|
+
export class TestNode implements INodeType {
|
|
43
|
+
description: INodeTypeDescription = {
|
|
44
|
+
displayName: 'Test Node',
|
|
45
|
+
name: 'testNode',
|
|
46
|
+
properties: [
|
|
47
|
+
{
|
|
48
|
+
displayName: 'Operation',
|
|
49
|
+
name: 'operation',
|
|
50
|
+
type: 'options',
|
|
51
|
+
options: [
|
|
52
|
+
{ name: 'Create', value: 'create' },
|
|
53
|
+
{ name: 'Delete', value: 'delete' },
|
|
54
|
+
{ name: 'Get', value: 'get' },
|
|
55
|
+
{ name: 'Update', value: 'update' },
|
|
56
|
+
],
|
|
57
|
+
default: 'get',
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
`,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'non-ASCII names sorted correctly via localeCompare',
|
|
66
|
+
filename: '/tmp/TestNode.node.ts',
|
|
67
|
+
code: `
|
|
68
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
69
|
+
|
|
70
|
+
export class TestNode implements INodeType {
|
|
71
|
+
description: INodeTypeDescription = {
|
|
72
|
+
displayName: 'Test Node',
|
|
73
|
+
name: 'testNode',
|
|
74
|
+
properties: [
|
|
75
|
+
{
|
|
76
|
+
displayName: 'Resource',
|
|
77
|
+
name: 'resource',
|
|
78
|
+
type: 'options',
|
|
79
|
+
options: [
|
|
80
|
+
{ name: 'Árbol', value: 'arbol' },
|
|
81
|
+
{ name: 'Empresa', value: 'empresa' },
|
|
82
|
+
{ name: 'Usuario', value: 'usuario' },
|
|
83
|
+
],
|
|
84
|
+
default: 'arbol',
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
`,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'single option is trivially sorted',
|
|
93
|
+
filename: '/tmp/TestNode.node.ts',
|
|
94
|
+
code: `
|
|
95
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
96
|
+
|
|
97
|
+
export class TestNode implements INodeType {
|
|
98
|
+
description: INodeTypeDescription = {
|
|
99
|
+
displayName: 'Test Node',
|
|
100
|
+
name: 'testNode',
|
|
101
|
+
properties: [
|
|
102
|
+
{
|
|
103
|
+
displayName: 'Resource',
|
|
104
|
+
name: 'resource',
|
|
105
|
+
type: 'options',
|
|
106
|
+
options: [
|
|
107
|
+
{ name: 'User', value: 'user' },
|
|
108
|
+
],
|
|
109
|
+
default: 'user',
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
`,
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: 'non-options type parameter is ignored',
|
|
118
|
+
filename: '/tmp/TestNode.node.ts',
|
|
119
|
+
code: `
|
|
120
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
121
|
+
|
|
122
|
+
export class TestNode implements INodeType {
|
|
123
|
+
description: INodeTypeDescription = {
|
|
124
|
+
displayName: 'Test Node',
|
|
125
|
+
name: 'testNode',
|
|
126
|
+
properties: [
|
|
127
|
+
{
|
|
128
|
+
displayName: 'Name',
|
|
129
|
+
name: 'name',
|
|
130
|
+
type: 'string',
|
|
131
|
+
default: '',
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
`,
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: 'non-node class is ignored',
|
|
140
|
+
filename: '/tmp/TestNode.node.ts',
|
|
141
|
+
code: `
|
|
142
|
+
export class NotANode {
|
|
143
|
+
description = {
|
|
144
|
+
properties: [
|
|
145
|
+
{
|
|
146
|
+
displayName: 'Resource',
|
|
147
|
+
name: 'resource',
|
|
148
|
+
type: 'options',
|
|
149
|
+
options: [
|
|
150
|
+
{ name: 'User', value: 'user' },
|
|
151
|
+
{ name: 'Contact', value: 'contact' },
|
|
152
|
+
],
|
|
153
|
+
default: 'user',
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
`,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: 'non-.node.ts file is ignored',
|
|
162
|
+
filename: '/tmp/TestHelper.ts',
|
|
163
|
+
code: `
|
|
164
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
165
|
+
|
|
166
|
+
export class TestNode implements INodeType {
|
|
167
|
+
description: INodeTypeDescription = {
|
|
168
|
+
displayName: 'Test Node',
|
|
169
|
+
name: 'testNode',
|
|
170
|
+
properties: [
|
|
171
|
+
{
|
|
172
|
+
displayName: 'Resource',
|
|
173
|
+
name: 'resource',
|
|
174
|
+
type: 'options',
|
|
175
|
+
options: [
|
|
176
|
+
{ name: 'User', value: 'user' },
|
|
177
|
+
{ name: 'Contact', value: 'contact' },
|
|
178
|
+
],
|
|
179
|
+
default: 'user',
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
`,
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: 'description assigned from a variable is skipped',
|
|
188
|
+
filename: '/tmp/TestNode.node.ts',
|
|
189
|
+
code: `
|
|
190
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
191
|
+
|
|
192
|
+
const desc: INodeTypeDescription = {} as INodeTypeDescription;
|
|
193
|
+
|
|
194
|
+
export class TestNode implements INodeType {
|
|
195
|
+
description = desc;
|
|
196
|
+
}
|
|
197
|
+
`,
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: 'node with no properties array is skipped',
|
|
201
|
+
filename: '/tmp/TestNode.node.ts',
|
|
202
|
+
code: `
|
|
203
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
204
|
+
|
|
205
|
+
export class TestNode implements INodeType {
|
|
206
|
+
description: INodeTypeDescription = {
|
|
207
|
+
displayName: 'Test Node',
|
|
208
|
+
name: 'testNode',
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
`,
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: 'spread element in properties array is skipped gracefully',
|
|
215
|
+
filename: '/tmp/TestNode.node.ts',
|
|
216
|
+
code: `
|
|
217
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
218
|
+
|
|
219
|
+
const extraProps = [{ displayName: 'Extra', name: 'extra', type: 'string', default: '' }];
|
|
220
|
+
|
|
221
|
+
export class TestNode implements INodeType {
|
|
222
|
+
description: INodeTypeDescription = {
|
|
223
|
+
displayName: 'Test Node',
|
|
224
|
+
name: 'testNode',
|
|
225
|
+
properties: [
|
|
226
|
+
...extraProps,
|
|
227
|
+
],
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
`,
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
name: 'options with a spread element are skipped (dynamic options)',
|
|
234
|
+
filename: '/tmp/TestNode.node.ts',
|
|
235
|
+
code: `
|
|
236
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
237
|
+
|
|
238
|
+
const extraOption = { name: 'Extra', value: 'extra' };
|
|
239
|
+
|
|
240
|
+
export class TestNode implements INodeType {
|
|
241
|
+
description: INodeTypeDescription = {
|
|
242
|
+
displayName: 'Test Node',
|
|
243
|
+
name: 'testNode',
|
|
244
|
+
properties: [
|
|
245
|
+
{
|
|
246
|
+
displayName: 'Resource',
|
|
247
|
+
name: 'resource',
|
|
248
|
+
type: 'options',
|
|
249
|
+
options: [
|
|
250
|
+
{ name: 'User', value: 'user' },
|
|
251
|
+
...([extraOption]),
|
|
252
|
+
],
|
|
253
|
+
default: 'user',
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
`,
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
name: 'options with a dynamic name value are skipped',
|
|
262
|
+
filename: '/tmp/TestNode.node.ts',
|
|
263
|
+
code: `
|
|
264
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
265
|
+
|
|
266
|
+
const dynamicName = 'Dynamic';
|
|
267
|
+
|
|
268
|
+
export class TestNode implements INodeType {
|
|
269
|
+
description: INodeTypeDescription = {
|
|
270
|
+
displayName: 'Test Node',
|
|
271
|
+
name: 'testNode',
|
|
272
|
+
properties: [
|
|
273
|
+
{
|
|
274
|
+
displayName: 'Resource',
|
|
275
|
+
name: 'resource',
|
|
276
|
+
type: 'options',
|
|
277
|
+
options: [
|
|
278
|
+
{ name: dynamicName, value: 'dynamic' },
|
|
279
|
+
{ name: 'User', value: 'user' },
|
|
280
|
+
],
|
|
281
|
+
default: 'user',
|
|
282
|
+
},
|
|
283
|
+
],
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
`,
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
invalid: [
|
|
290
|
+
{
|
|
291
|
+
name: 'resource options not sorted alphabetically',
|
|
292
|
+
filename: '/tmp/TestNode.node.ts',
|
|
293
|
+
code: `
|
|
294
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
295
|
+
|
|
296
|
+
export class TestNode implements INodeType {
|
|
297
|
+
description: INodeTypeDescription = {
|
|
298
|
+
displayName: 'Test Node',
|
|
299
|
+
name: 'testNode',
|
|
300
|
+
properties: [
|
|
301
|
+
{
|
|
302
|
+
displayName: 'Resource',
|
|
303
|
+
name: 'resource',
|
|
304
|
+
type: 'options',
|
|
305
|
+
options: [
|
|
306
|
+
{ name: 'User', value: 'user' },
|
|
307
|
+
{ name: 'Contact', value: 'contact' },
|
|
308
|
+
{ name: 'Project', value: 'project' },
|
|
309
|
+
],
|
|
310
|
+
default: 'user',
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
`,
|
|
316
|
+
errors: [
|
|
317
|
+
{
|
|
318
|
+
messageId: 'optionsNotSorted',
|
|
319
|
+
data: { displayName: 'Resource', expectedOrder: 'Contact, Project, User' },
|
|
320
|
+
},
|
|
321
|
+
],
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
name: 'operation options not sorted alphabetically',
|
|
325
|
+
filename: '/tmp/TestNode.node.ts',
|
|
326
|
+
code: `
|
|
327
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
328
|
+
|
|
329
|
+
export class TestNode implements INodeType {
|
|
330
|
+
description: INodeTypeDescription = {
|
|
331
|
+
displayName: 'Test Node',
|
|
332
|
+
name: 'testNode',
|
|
333
|
+
properties: [
|
|
334
|
+
{
|
|
335
|
+
displayName: 'Operation',
|
|
336
|
+
name: 'operation',
|
|
337
|
+
type: 'options',
|
|
338
|
+
options: [
|
|
339
|
+
{ name: 'Update', value: 'update' },
|
|
340
|
+
{ name: 'Create', value: 'create' },
|
|
341
|
+
{ name: 'Delete', value: 'delete' },
|
|
342
|
+
{ name: 'Get', value: 'get' },
|
|
343
|
+
],
|
|
344
|
+
default: 'get',
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
`,
|
|
350
|
+
errors: [
|
|
351
|
+
{
|
|
352
|
+
messageId: 'optionsNotSorted',
|
|
353
|
+
data: { displayName: 'Operation', expectedOrder: 'Create, Delete, Get, Update' },
|
|
354
|
+
},
|
|
355
|
+
],
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
name: 'other options-type parameter not sorted alphabetically',
|
|
359
|
+
filename: '/tmp/TestNode.node.ts',
|
|
360
|
+
code: `
|
|
361
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
362
|
+
|
|
363
|
+
export class TestNode implements INodeType {
|
|
364
|
+
description: INodeTypeDescription = {
|
|
365
|
+
displayName: 'Test Node',
|
|
366
|
+
name: 'testNode',
|
|
367
|
+
properties: [
|
|
368
|
+
{
|
|
369
|
+
displayName: 'Model',
|
|
370
|
+
name: 'model',
|
|
371
|
+
type: 'options',
|
|
372
|
+
options: [
|
|
373
|
+
{ name: 'GPT-4', value: 'gpt-4' },
|
|
374
|
+
{ name: 'Claude', value: 'claude' },
|
|
375
|
+
{ name: 'Gemini', value: 'gemini' },
|
|
376
|
+
],
|
|
377
|
+
default: 'gpt-4',
|
|
378
|
+
},
|
|
379
|
+
],
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
`,
|
|
383
|
+
errors: [
|
|
384
|
+
{
|
|
385
|
+
messageId: 'optionsNotSorted',
|
|
386
|
+
data: { displayName: 'Model', expectedOrder: 'Claude, Gemini, GPT-4' },
|
|
387
|
+
},
|
|
388
|
+
],
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
name: 'options-type parameter without displayName falls back to "unknown"',
|
|
392
|
+
filename: '/tmp/TestNode.node.ts',
|
|
393
|
+
code: `
|
|
394
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
395
|
+
|
|
396
|
+
export class TestNode implements INodeType {
|
|
397
|
+
description: INodeTypeDescription = {
|
|
398
|
+
displayName: 'Test Node',
|
|
399
|
+
name: 'testNode',
|
|
400
|
+
properties: [
|
|
401
|
+
{
|
|
402
|
+
name: 'resource',
|
|
403
|
+
type: 'options',
|
|
404
|
+
options: [
|
|
405
|
+
{ name: 'User', value: 'user' },
|
|
406
|
+
{ name: 'Contact', value: 'contact' },
|
|
407
|
+
],
|
|
408
|
+
default: 'user',
|
|
409
|
+
},
|
|
410
|
+
],
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
`,
|
|
414
|
+
errors: [
|
|
415
|
+
{
|
|
416
|
+
messageId: 'optionsNotSorted',
|
|
417
|
+
data: { displayName: 'unknown', expectedOrder: 'Contact, User' },
|
|
418
|
+
},
|
|
419
|
+
],
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
name: 'multiple unsorted parameters each report an error',
|
|
423
|
+
filename: '/tmp/TestNode.node.ts',
|
|
424
|
+
code: `
|
|
425
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
426
|
+
|
|
427
|
+
export class TestNode implements INodeType {
|
|
428
|
+
description: INodeTypeDescription = {
|
|
429
|
+
displayName: 'Test Node',
|
|
430
|
+
name: 'testNode',
|
|
431
|
+
properties: [
|
|
432
|
+
{
|
|
433
|
+
displayName: 'Resource',
|
|
434
|
+
name: 'resource',
|
|
435
|
+
type: 'options',
|
|
436
|
+
options: [
|
|
437
|
+
{ name: 'User', value: 'user' },
|
|
438
|
+
{ name: 'Contact', value: 'contact' },
|
|
439
|
+
],
|
|
440
|
+
default: 'user',
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
displayName: 'Operation',
|
|
444
|
+
name: 'operation',
|
|
445
|
+
type: 'options',
|
|
446
|
+
options: [
|
|
447
|
+
{ name: 'Update', value: 'update' },
|
|
448
|
+
{ name: 'Create', value: 'create' },
|
|
449
|
+
],
|
|
450
|
+
default: 'create',
|
|
451
|
+
},
|
|
452
|
+
],
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
`,
|
|
456
|
+
errors: [
|
|
457
|
+
{
|
|
458
|
+
messageId: 'optionsNotSorted',
|
|
459
|
+
data: { displayName: 'Resource', expectedOrder: 'Contact, User' },
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
messageId: 'optionsNotSorted',
|
|
463
|
+
data: { displayName: 'Operation', expectedOrder: 'Create, Update' },
|
|
464
|
+
},
|
|
465
|
+
],
|
|
466
|
+
},
|
|
467
|
+
],
|
|
468
|
+
});
|