@n8n/eslint-plugin-community-nodes 0.15.0 → 0.16.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 +5 -0
- package/dist/plugin.d.ts +36 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +12 -0
- package/dist/plugin.js.map +1 -1
- package/dist/rules/cred-class-name-suffix.d.ts +2 -0
- package/dist/rules/cred-class-name-suffix.d.ts.map +1 -0
- package/dist/rules/cred-class-name-suffix.js +53 -0
- package/dist/rules/cred-class-name-suffix.js.map +1 -0
- package/dist/rules/cred-class-oauth2-naming.d.ts +2 -0
- package/dist/rules/cred-class-oauth2-naming.d.ts.map +1 -0
- package/dist/rules/cred-class-oauth2-naming.js +96 -0
- package/dist/rules/cred-class-oauth2-naming.js.map +1 -0
- package/dist/rules/index.d.ts +8 -0
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +12 -0
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/n8n-object-validation.d.ts +5 -0
- package/dist/rules/n8n-object-validation.d.ts.map +1 -0
- package/dist/rules/n8n-object-validation.js +148 -0
- package/dist/rules/n8n-object-validation.js.map +1 -0
- package/dist/rules/no-builder-hint-leakage.d.ts +7 -0
- package/dist/rules/no-builder-hint-leakage.d.ts.map +1 -0
- package/dist/rules/no-builder-hint-leakage.js +99 -0
- package/dist/rules/no-builder-hint-leakage.js.map +1 -0
- package/dist/rules/no-overrides-field.js +1 -1
- package/dist/rules/no-overrides-field.js.map +1 -1
- package/dist/rules/no-template-placeholders.d.ts +2 -0
- package/dist/rules/no-template-placeholders.d.ts.map +1 -0
- package/dist/rules/no-template-placeholders.js +57 -0
- package/dist/rules/no-template-placeholders.js.map +1 -0
- package/dist/rules/node-operation-error-itemindex.d.ts +12 -0
- package/dist/rules/node-operation-error-itemindex.d.ts.map +1 -0
- package/dist/rules/node-operation-error-itemindex.js +184 -0
- package/dist/rules/node-operation-error-itemindex.js.map +1 -0
- package/dist/utils/ast-utils.d.ts.map +1 -1
- package/dist/utils/ast-utils.js +5 -1
- package/dist/utils/ast-utils.js.map +1 -1
- package/docs/rules/cred-class-name-suffix.md +46 -0
- package/docs/rules/cred-class-oauth2-naming.md +68 -0
- package/docs/rules/n8n-object-validation.md +93 -0
- package/docs/rules/no-overrides-field.md +5 -5
- package/docs/rules/no-template-placeholders.md +51 -0
- package/docs/rules/node-operation-error-itemindex.md +81 -0
- package/package.json +2 -2
- package/src/plugin.ts +12 -0
- package/src/rules/cred-class-name-suffix.test.ts +74 -0
- package/src/rules/cred-class-name-suffix.ts +57 -0
- package/src/rules/cred-class-oauth2-naming.test.ts +197 -0
- package/src/rules/cred-class-oauth2-naming.ts +118 -0
- package/src/rules/index.ts +12 -0
- package/src/rules/n8n-object-validation.test.ts +202 -0
- package/src/rules/n8n-object-validation.ts +200 -0
- package/src/rules/no-builder-hint-leakage.test.ts +84 -0
- package/src/rules/no-builder-hint-leakage.ts +112 -0
- package/src/rules/no-overrides-field.ts +1 -1
- package/src/rules/no-template-placeholders.test.ts +135 -0
- package/src/rules/no-template-placeholders.ts +68 -0
- package/src/rules/node-operation-error-itemindex.test.ts +280 -0
- package/src/rules/node-operation-error-itemindex.ts +223 -0
- package/src/utils/ast-utils.ts +5 -1
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
|
|
3
|
+
import { CredClassOAuth2NamingRule } from './cred-class-oauth2-naming.js';
|
|
4
|
+
|
|
5
|
+
const ruleTester = new RuleTester();
|
|
6
|
+
|
|
7
|
+
const credFilePath = '/tmp/TestCredential.credentials.ts';
|
|
8
|
+
const nonCredFilePath = '/tmp/SomeHelper.ts';
|
|
9
|
+
|
|
10
|
+
type CredentialFields = {
|
|
11
|
+
className: string;
|
|
12
|
+
name?: string;
|
|
13
|
+
displayName?: string;
|
|
14
|
+
extendsValues?: string[];
|
|
15
|
+
superClass?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function createCredentialCode(fields: CredentialFields): string {
|
|
19
|
+
const { className, name, displayName, extendsValues, superClass } = fields;
|
|
20
|
+
|
|
21
|
+
const heritage = superClass ? ` extends ${superClass}` : '';
|
|
22
|
+
const lines: string[] = [];
|
|
23
|
+
if (name !== undefined) lines.push(`\tname = '${name}';`);
|
|
24
|
+
if (displayName !== undefined) lines.push(`\tdisplayName = '${displayName}';`);
|
|
25
|
+
if (extendsValues !== undefined) {
|
|
26
|
+
const arr = extendsValues.map((v) => `'${v}'`).join(', ');
|
|
27
|
+
lines.push(`\textends = [${arr}];`);
|
|
28
|
+
}
|
|
29
|
+
lines.push('\tproperties: INodeProperties[] = [];');
|
|
30
|
+
|
|
31
|
+
return `
|
|
32
|
+
import type { ICredentialType, INodeProperties } from 'n8n-workflow';
|
|
33
|
+
|
|
34
|
+
export class ${className}${heritage} implements ICredentialType {
|
|
35
|
+
${lines.join('\n')}
|
|
36
|
+
}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createRegularClass(): string {
|
|
40
|
+
return `
|
|
41
|
+
export class SomeHelper {
|
|
42
|
+
name = 'helper';
|
|
43
|
+
}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
ruleTester.run('cred-class-oauth2-naming', CredClassOAuth2NamingRule, {
|
|
47
|
+
valid: [
|
|
48
|
+
{
|
|
49
|
+
name: 'non-OAuth2 credential is ignored',
|
|
50
|
+
filename: credFilePath,
|
|
51
|
+
code: createCredentialCode({
|
|
52
|
+
className: 'GoogleApi',
|
|
53
|
+
name: 'googleApi',
|
|
54
|
+
displayName: 'Google API',
|
|
55
|
+
}),
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'OAuth2 credential with all naming correct',
|
|
59
|
+
filename: credFilePath,
|
|
60
|
+
code: createCredentialCode({
|
|
61
|
+
className: 'GoogleOAuth2Api',
|
|
62
|
+
name: 'googleOAuth2Api',
|
|
63
|
+
displayName: 'Google OAuth2 API',
|
|
64
|
+
}),
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'OAuth2 credential detected via extends array, all naming correct',
|
|
68
|
+
filename: credFilePath,
|
|
69
|
+
code: createCredentialCode({
|
|
70
|
+
className: 'GoogleOAuth2Api',
|
|
71
|
+
name: 'googleOAuth2Api',
|
|
72
|
+
displayName: 'Google OAuth2 API',
|
|
73
|
+
extendsValues: ['oAuth2Api'],
|
|
74
|
+
}),
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'OAuth2 credential detected via TS superClass, all naming correct',
|
|
78
|
+
filename: credFilePath,
|
|
79
|
+
code: createCredentialCode({
|
|
80
|
+
className: 'GoogleOAuth2Api',
|
|
81
|
+
name: 'googleOAuth2Api',
|
|
82
|
+
displayName: 'Google OAuth2 API',
|
|
83
|
+
superClass: 'OAuth2Api',
|
|
84
|
+
}),
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'class not implementing ICredentialType is ignored',
|
|
88
|
+
filename: credFilePath,
|
|
89
|
+
code: createRegularClass(),
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'non-.credentials.ts file is ignored',
|
|
93
|
+
filename: nonCredFilePath,
|
|
94
|
+
code: createCredentialCode({
|
|
95
|
+
className: 'GoogleApi',
|
|
96
|
+
name: 'googleOAuth2Api',
|
|
97
|
+
displayName: 'Google OAuth2 API',
|
|
98
|
+
}),
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
invalid: [
|
|
102
|
+
{
|
|
103
|
+
name: 'class name missing OAuth2 (detected via name field), with Api suffix',
|
|
104
|
+
filename: credFilePath,
|
|
105
|
+
code: createCredentialCode({
|
|
106
|
+
className: 'GoogleApi',
|
|
107
|
+
name: 'googleOAuth2Api',
|
|
108
|
+
displayName: 'Google OAuth2 API',
|
|
109
|
+
}),
|
|
110
|
+
errors: [{ messageId: 'classNameMissingOAuth2', data: { name: 'GoogleApi' } }],
|
|
111
|
+
output: createCredentialCode({
|
|
112
|
+
className: 'GoogleOAuth2Api',
|
|
113
|
+
name: 'googleOAuth2Api',
|
|
114
|
+
displayName: 'Google OAuth2 API',
|
|
115
|
+
}),
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'class name missing OAuth2 (detected via displayName), no Api suffix',
|
|
119
|
+
filename: credFilePath,
|
|
120
|
+
code: createCredentialCode({
|
|
121
|
+
className: 'Google',
|
|
122
|
+
name: 'googleOAuth2Api',
|
|
123
|
+
displayName: 'Google OAuth2 API',
|
|
124
|
+
}),
|
|
125
|
+
errors: [{ messageId: 'classNameMissingOAuth2', data: { name: 'Google' } }],
|
|
126
|
+
output: createCredentialCode({
|
|
127
|
+
className: 'GoogleOAuth2Api',
|
|
128
|
+
name: 'googleOAuth2Api',
|
|
129
|
+
displayName: 'Google OAuth2 API',
|
|
130
|
+
}),
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'name field missing OAuth2 (detected via class name)',
|
|
134
|
+
filename: credFilePath,
|
|
135
|
+
code: createCredentialCode({
|
|
136
|
+
className: 'GoogleOAuth2Api',
|
|
137
|
+
name: 'googleApi',
|
|
138
|
+
displayName: 'Google OAuth2 API',
|
|
139
|
+
}),
|
|
140
|
+
errors: [{ messageId: 'nameMissingOAuth2', data: { value: 'googleApi' } }],
|
|
141
|
+
output: null,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: 'displayName missing OAuth2 (detected via class name)',
|
|
145
|
+
filename: credFilePath,
|
|
146
|
+
code: createCredentialCode({
|
|
147
|
+
className: 'GoogleOAuth2Api',
|
|
148
|
+
name: 'googleOAuth2Api',
|
|
149
|
+
displayName: 'Google API',
|
|
150
|
+
}),
|
|
151
|
+
errors: [{ messageId: 'displayNameMissingOAuth2', data: { value: 'Google API' } }],
|
|
152
|
+
output: null,
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: 'all three missing OAuth2, detected via extends array',
|
|
156
|
+
filename: credFilePath,
|
|
157
|
+
code: createCredentialCode({
|
|
158
|
+
className: 'GoogleApi',
|
|
159
|
+
name: 'googleApi',
|
|
160
|
+
displayName: 'Google API',
|
|
161
|
+
extendsValues: ['oAuth2Api'],
|
|
162
|
+
}),
|
|
163
|
+
errors: [
|
|
164
|
+
{ messageId: 'classNameMissingOAuth2', data: { name: 'GoogleApi' } },
|
|
165
|
+
{ messageId: 'nameMissingOAuth2', data: { value: 'googleApi' } },
|
|
166
|
+
{ messageId: 'displayNameMissingOAuth2', data: { value: 'Google API' } },
|
|
167
|
+
],
|
|
168
|
+
output: createCredentialCode({
|
|
169
|
+
className: 'GoogleOAuth2Api',
|
|
170
|
+
name: 'googleApi',
|
|
171
|
+
displayName: 'Google API',
|
|
172
|
+
extendsValues: ['oAuth2Api'],
|
|
173
|
+
}),
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: 'all three missing OAuth2, detected via TS superClass extends',
|
|
177
|
+
filename: credFilePath,
|
|
178
|
+
code: createCredentialCode({
|
|
179
|
+
className: 'GoogleApi',
|
|
180
|
+
name: 'googleApi',
|
|
181
|
+
displayName: 'Google API',
|
|
182
|
+
superClass: 'OAuth2Api',
|
|
183
|
+
}),
|
|
184
|
+
errors: [
|
|
185
|
+
{ messageId: 'classNameMissingOAuth2', data: { name: 'GoogleApi' } },
|
|
186
|
+
{ messageId: 'nameMissingOAuth2', data: { value: 'googleApi' } },
|
|
187
|
+
{ messageId: 'displayNameMissingOAuth2', data: { value: 'Google API' } },
|
|
188
|
+
],
|
|
189
|
+
output: createCredentialCode({
|
|
190
|
+
className: 'GoogleOAuth2Api',
|
|
191
|
+
name: 'googleApi',
|
|
192
|
+
displayName: 'Google API',
|
|
193
|
+
superClass: 'OAuth2Api',
|
|
194
|
+
}),
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
createRule,
|
|
6
|
+
findClassProperty,
|
|
7
|
+
getStringLiteralValue,
|
|
8
|
+
isCredentialTypeClass,
|
|
9
|
+
isFileType,
|
|
10
|
+
} from '../utils/index.js';
|
|
11
|
+
|
|
12
|
+
const OAUTH2_PATTERN = /oauth2/i;
|
|
13
|
+
|
|
14
|
+
function containsOAuth2(value: string): boolean {
|
|
15
|
+
return OAUTH2_PATTERN.test(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getExtendsArrayValues(node: TSESTree.PropertyDefinition): string[] {
|
|
19
|
+
if (node.value?.type !== AST_NODE_TYPES.ArrayExpression) return [];
|
|
20
|
+
const values: string[] = [];
|
|
21
|
+
for (const element of node.value.elements) {
|
|
22
|
+
const value = element ? getStringLiteralValue(element) : null;
|
|
23
|
+
if (value !== null) values.push(value);
|
|
24
|
+
}
|
|
25
|
+
return values;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function suggestOAuth2ApiName(className: string): string {
|
|
29
|
+
if (className.endsWith('Api')) {
|
|
30
|
+
return `${className.slice(0, -'Api'.length)}OAuth2Api`;
|
|
31
|
+
}
|
|
32
|
+
return `${className}OAuth2Api`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const CredClassOAuth2NamingRule = createRule({
|
|
36
|
+
name: 'cred-class-oauth2-naming',
|
|
37
|
+
meta: {
|
|
38
|
+
type: 'problem',
|
|
39
|
+
docs: {
|
|
40
|
+
description:
|
|
41
|
+
'OAuth2 credentials must include `OAuth2` in the class name, `name`, and `displayName`',
|
|
42
|
+
},
|
|
43
|
+
messages: {
|
|
44
|
+
classNameMissingOAuth2: "OAuth2 credential class name '{{name}}' must end with 'OAuth2Api'",
|
|
45
|
+
nameMissingOAuth2: "OAuth2 credential `name` field '{{value}}' must contain 'OAuth2'",
|
|
46
|
+
displayNameMissingOAuth2:
|
|
47
|
+
"OAuth2 credential `displayName` field '{{value}}' must contain 'OAuth2'",
|
|
48
|
+
},
|
|
49
|
+
schema: [],
|
|
50
|
+
fixable: 'code',
|
|
51
|
+
},
|
|
52
|
+
defaultOptions: [],
|
|
53
|
+
create(context) {
|
|
54
|
+
if (!isFileType(context.filename, '.credentials.ts')) {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
ClassDeclaration(node) {
|
|
60
|
+
if (!isCredentialTypeClass(node)) return;
|
|
61
|
+
if (!node.id) return;
|
|
62
|
+
|
|
63
|
+
const className = node.id.name;
|
|
64
|
+
const superClassName =
|
|
65
|
+
node.superClass?.type === AST_NODE_TYPES.Identifier ? node.superClass.name : null;
|
|
66
|
+
|
|
67
|
+
const nameProperty = findClassProperty(node, 'name');
|
|
68
|
+
const nameValue = nameProperty?.value ? getStringLiteralValue(nameProperty.value) : null;
|
|
69
|
+
|
|
70
|
+
const displayNameProperty = findClassProperty(node, 'displayName');
|
|
71
|
+
const displayNameValue = displayNameProperty?.value
|
|
72
|
+
? getStringLiteralValue(displayNameProperty.value)
|
|
73
|
+
: null;
|
|
74
|
+
|
|
75
|
+
const extendsProperty = findClassProperty(node, 'extends');
|
|
76
|
+
const extendsValues = extendsProperty ? getExtendsArrayValues(extendsProperty) : [];
|
|
77
|
+
|
|
78
|
+
const isOAuth2Credential =
|
|
79
|
+
containsOAuth2(className) ||
|
|
80
|
+
(superClassName !== null && containsOAuth2(superClassName)) ||
|
|
81
|
+
extendsValues.some(containsOAuth2) ||
|
|
82
|
+
(nameValue !== null && containsOAuth2(nameValue)) ||
|
|
83
|
+
(displayNameValue !== null && containsOAuth2(displayNameValue));
|
|
84
|
+
|
|
85
|
+
if (!isOAuth2Credential) return;
|
|
86
|
+
|
|
87
|
+
if (!className.endsWith('OAuth2Api')) {
|
|
88
|
+
const fixedClassName = suggestOAuth2ApiName(className);
|
|
89
|
+
|
|
90
|
+
context.report({
|
|
91
|
+
node: node.id,
|
|
92
|
+
messageId: 'classNameMissingOAuth2',
|
|
93
|
+
data: { name: className },
|
|
94
|
+
fix(fixer) {
|
|
95
|
+
return fixer.replaceText(node.id!, fixedClassName);
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (nameValue !== null && !containsOAuth2(nameValue)) {
|
|
101
|
+
context.report({
|
|
102
|
+
node: nameProperty!.value!,
|
|
103
|
+
messageId: 'nameMissingOAuth2',
|
|
104
|
+
data: { value: nameValue },
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (displayNameValue !== null && !containsOAuth2(displayNameValue)) {
|
|
109
|
+
context.report({
|
|
110
|
+
node: displayNameProperty!.value!,
|
|
111
|
+
messageId: 'displayNameMissingOAuth2',
|
|
112
|
+
data: { value: displayNameValue },
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
},
|
|
118
|
+
});
|
package/src/rules/index.ts
CHANGED
|
@@ -2,11 +2,15 @@ import type { AnyRuleModule } from '@typescript-eslint/utils/ts-eslint';
|
|
|
2
2
|
|
|
3
3
|
import { AiNodePackageJsonRule } from './ai-node-package-json.js';
|
|
4
4
|
import { CredClassFieldIconMissingRule } from './cred-class-field-icon-missing.js';
|
|
5
|
+
import { CredClassNameSuffixRule } from './cred-class-name-suffix.js';
|
|
6
|
+
import { CredClassOAuth2NamingRule } from './cred-class-oauth2-naming.js';
|
|
5
7
|
import { CredentialDocumentationUrlRule } from './credential-documentation-url.js';
|
|
6
8
|
import { CredentialPasswordFieldRule } from './credential-password-field.js';
|
|
7
9
|
import { CredentialTestRequiredRule } from './credential-test-required.js';
|
|
8
10
|
import { IconValidationRule } from './icon-validation.js';
|
|
9
11
|
import { MissingPairedItemRule } from './missing-paired-item.js';
|
|
12
|
+
import { N8nObjectValidationRule } from './n8n-object-validation.js';
|
|
13
|
+
import { NoBuilderHintLeakageRule } from './no-builder-hint-leakage.js';
|
|
10
14
|
import { NoCredentialReuseRule } from './no-credential-reuse.js';
|
|
11
15
|
import { NoDeprecatedWorkflowFunctionsRule } from './no-deprecated-workflow-functions.js';
|
|
12
16
|
import { NoForbiddenLifecycleScriptsRule } from './no-forbidden-lifecycle-scripts.js';
|
|
@@ -15,8 +19,10 @@ import { NoOverridesFieldRule } from './no-overrides-field.js';
|
|
|
15
19
|
import { NoRestrictedGlobalsRule } from './no-restricted-globals.js';
|
|
16
20
|
import { NoRestrictedImportsRule } from './no-restricted-imports.js';
|
|
17
21
|
import { NoRuntimeDependenciesRule } from './no-runtime-dependencies.js';
|
|
22
|
+
import { NoTemplatePlaceholdersRule } from './no-template-placeholders.js';
|
|
18
23
|
import { NodeClassDescriptionIconMissingRule } from './node-class-description-icon-missing.js';
|
|
19
24
|
import { NodeConnectionTypeLiteralRule } from './node-connection-type-literal.js';
|
|
25
|
+
import { NodeOperationErrorItemIndexRule } from './node-operation-error-itemindex.js';
|
|
20
26
|
import { NodeUsableAsToolRule } from './node-usable-as-tool.js';
|
|
21
27
|
import { OptionsSortedAlphabeticallyRule } from './options-sorted-alphabetically.js';
|
|
22
28
|
import { PackageNameConventionRule } from './package-name-convention.js';
|
|
@@ -44,13 +50,19 @@ export const rules = {
|
|
|
44
50
|
'no-http-request-with-manual-auth': NoHttpRequestWithManualAuthRule,
|
|
45
51
|
'no-overrides-field': NoOverridesFieldRule,
|
|
46
52
|
'no-runtime-dependencies': NoRuntimeDependenciesRule,
|
|
53
|
+
'no-template-placeholders': NoTemplatePlaceholdersRule,
|
|
47
54
|
'icon-validation': IconValidationRule,
|
|
48
55
|
'resource-operation-pattern': ResourceOperationPatternRule,
|
|
49
56
|
'credential-documentation-url': CredentialDocumentationUrlRule,
|
|
50
57
|
'node-class-description-icon-missing': NodeClassDescriptionIconMissingRule,
|
|
51
58
|
'cred-class-field-icon-missing': CredClassFieldIconMissingRule,
|
|
59
|
+
'cred-class-name-suffix': CredClassNameSuffixRule,
|
|
60
|
+
'cred-class-oauth2-naming': CredClassOAuth2NamingRule,
|
|
52
61
|
'node-connection-type-literal': NodeConnectionTypeLiteralRule,
|
|
62
|
+
'node-operation-error-itemindex': NodeOperationErrorItemIndexRule,
|
|
53
63
|
'missing-paired-item': MissingPairedItemRule,
|
|
64
|
+
'no-builder-hint-leakage': NoBuilderHintLeakageRule,
|
|
65
|
+
'n8n-object-validation': N8nObjectValidationRule,
|
|
54
66
|
'require-community-node-keyword': RequireCommunityNodeKeywordRule,
|
|
55
67
|
'require-continue-on-fail': RequireContinueOnFailRule,
|
|
56
68
|
'require-node-api-error': RequireNodeApiErrorRule,
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
|
|
3
|
+
import { N8nObjectValidationRule } from './n8n-object-validation.js';
|
|
4
|
+
|
|
5
|
+
const ruleTester = new RuleTester();
|
|
6
|
+
|
|
7
|
+
ruleTester.run('n8n-object-validation', N8nObjectValidationRule, {
|
|
8
|
+
valid: [
|
|
9
|
+
{
|
|
10
|
+
name: 'minimal valid n8n object with one node path',
|
|
11
|
+
filename: 'package.json',
|
|
12
|
+
code: '{ "name": "n8n-nodes-example", "n8n": { "n8nNodesApiVersion": 1, "nodes": ["dist/nodes/Foo/Foo.node.js"] } }',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: 'valid n8n object with credentials',
|
|
16
|
+
filename: 'package.json',
|
|
17
|
+
code: '{ "name": "n8n-nodes-example", "n8n": { "n8nNodesApiVersion": 1, "nodes": ["dist/nodes/Foo/Foo.node.js"], "credentials": ["dist/credentials/Foo.credentials.js"] } }',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'empty credentials array is allowed',
|
|
21
|
+
filename: 'package.json',
|
|
22
|
+
code: '{ "name": "n8n-nodes-example", "n8n": { "n8nNodesApiVersion": 1, "nodes": ["dist/x.js"], "credentials": [] } }',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'higher integer api version is allowed',
|
|
26
|
+
filename: 'package.json',
|
|
27
|
+
code: '{ "name": "n8n-nodes-example", "n8n": { "n8nNodesApiVersion": 2, "nodes": ["dist/x.js"] } }',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'non-package.json file is ignored',
|
|
31
|
+
filename: 'some-config.json',
|
|
32
|
+
code: '{ "n8n": null }',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'nested "n8n" key inside another field is allowed (only root is validated)',
|
|
36
|
+
filename: 'package.json',
|
|
37
|
+
code: '{ "name": "n8n-nodes-example", "n8n": { "n8nNodesApiVersion": 1, "nodes": ["dist/x.js"] }, "config": { "n8n": "ignored" } }',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'objects nested inside arrays are not treated as the package root',
|
|
41
|
+
filename: 'package.json',
|
|
42
|
+
code: '{ "name": "n8n-nodes-example", "n8n": { "n8nNodesApiVersion": 1, "nodes": ["dist/x.js"] }, "contributors": [{ "name": "Jane" }, { "name": "John" }] }',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'strict is true',
|
|
46
|
+
filename: 'package.json',
|
|
47
|
+
code: '{ "name": "n8n-nodes-example", "n8n": { "n8nNodesApiVersion": 1, "nodes": ["dist/x.js"], "strict": true } }',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'strict is false',
|
|
51
|
+
filename: 'package.json',
|
|
52
|
+
code: '{ "name": "n8n-nodes-example", "n8n": { "n8nNodesApiVersion": 1, "nodes": ["dist/x.js"], "strict": false } }',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'strict is omitted (optional field)',
|
|
56
|
+
filename: 'package.json',
|
|
57
|
+
code: '{ "name": "n8n-nodes-example", "n8n": { "n8nNodesApiVersion": 1, "nodes": ["dist/x.js"] } }',
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
invalid: [
|
|
61
|
+
{
|
|
62
|
+
name: 'missing n8n object entirely',
|
|
63
|
+
filename: 'package.json',
|
|
64
|
+
code: '{ "name": "n8n-nodes-example", "version": "1.0.0" }',
|
|
65
|
+
errors: [{ messageId: 'missingN8nObject' }],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'n8n value is not an object',
|
|
69
|
+
filename: 'package.json',
|
|
70
|
+
code: '{ "name": "n8n-nodes-example", "n8n": "not-an-object" }',
|
|
71
|
+
errors: [{ messageId: 'missingN8nObject' }],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'missing n8nNodesApiVersion',
|
|
75
|
+
filename: 'package.json',
|
|
76
|
+
code: '{ "name": "n8n-nodes-example", "n8n": { "nodes": ["dist/x.js"] } }',
|
|
77
|
+
errors: [{ messageId: 'missingNodesApiVersion' }],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'n8nNodesApiVersion is a string',
|
|
81
|
+
filename: 'package.json',
|
|
82
|
+
code: '{ "name": "n8n-nodes-example", "n8n": { "n8nNodesApiVersion": "1", "nodes": ["dist/x.js"] } }',
|
|
83
|
+
errors: [{ messageId: 'invalidNodesApiVersion', data: { value: '1' } }],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'n8nNodesApiVersion is zero',
|
|
87
|
+
filename: 'package.json',
|
|
88
|
+
code: '{ "name": "n8n-nodes-example", "n8n": { "n8nNodesApiVersion": 0, "nodes": ["dist/x.js"] } }',
|
|
89
|
+
errors: [{ messageId: 'invalidNodesApiVersion', data: { value: '0' } }],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'n8nNodesApiVersion is a float',
|
|
93
|
+
filename: 'package.json',
|
|
94
|
+
code: '{ "name": "n8n-nodes-example", "n8n": { "n8nNodesApiVersion": 1.5, "nodes": ["dist/x.js"] } }',
|
|
95
|
+
errors: [{ messageId: 'invalidNodesApiVersion', data: { value: '1.5' } }],
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: 'n8nNodesApiVersion is negative',
|
|
99
|
+
filename: 'package.json',
|
|
100
|
+
code: '{ "name": "n8n-nodes-example", "n8n": { "n8nNodesApiVersion": -1, "nodes": ["dist/x.js"] } }',
|
|
101
|
+
errors: [{ messageId: 'invalidNodesApiVersion' }],
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: 'n8nNodesApiVersion at root level instead of inside n8n',
|
|
105
|
+
filename: 'package.json',
|
|
106
|
+
code: '{ "name": "n8n-nodes-example", "n8nNodesApiVersion": 1, "n8n": { "nodes": ["dist/x.js"] } }',
|
|
107
|
+
errors: [{ messageId: 'wrongLocationApiVersion' }, { messageId: 'missingNodesApiVersion' }],
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'missing nodes array',
|
|
111
|
+
filename: 'package.json',
|
|
112
|
+
code: '{ "name": "n8n-nodes-example", "n8n": { "n8nNodesApiVersion": 1 } }',
|
|
113
|
+
errors: [{ messageId: 'missingN8nNodes' }],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: 'nodes is not an array',
|
|
117
|
+
filename: 'package.json',
|
|
118
|
+
code: '{ "name": "n8n-nodes-example", "n8n": { "n8nNodesApiVersion": 1, "nodes": "dist/x.js" } }',
|
|
119
|
+
errors: [{ messageId: 'n8nNodesNotArray' }],
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'nodes is empty array',
|
|
123
|
+
filename: 'package.json',
|
|
124
|
+
code: '{ "name": "n8n-nodes-example", "n8n": { "n8nNodesApiVersion": 1, "nodes": [] } }',
|
|
125
|
+
errors: [{ messageId: 'emptyN8nNodes' }],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: 'node path does not start with dist/',
|
|
129
|
+
filename: 'package.json',
|
|
130
|
+
code: '{ "name": "n8n-nodes-example", "n8n": { "n8nNodesApiVersion": 1, "nodes": ["nodes/Foo/Foo.node.js"] } }',
|
|
131
|
+
errors: [{ messageId: 'nodePathNotInDist', data: { path: 'nodes/Foo/Foo.node.js' } }],
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: 'node path uses ./dist/ prefix instead of dist/',
|
|
135
|
+
filename: 'package.json',
|
|
136
|
+
code: '{ "name": "n8n-nodes-example", "n8n": { "n8nNodesApiVersion": 1, "nodes": ["./dist/x.js"] } }',
|
|
137
|
+
errors: [{ messageId: 'nodePathNotInDist', data: { path: './dist/x.js' } }],
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: 'node path with wrong casing',
|
|
141
|
+
filename: 'package.json',
|
|
142
|
+
code: '{ "name": "n8n-nodes-example", "n8n": { "n8nNodesApiVersion": 1, "nodes": ["DIST/x.js"] } }',
|
|
143
|
+
errors: [{ messageId: 'nodePathNotInDist', data: { path: 'DIST/x.js' } }],
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: 'node path is not a string',
|
|
147
|
+
filename: 'package.json',
|
|
148
|
+
code: '{ "name": "n8n-nodes-example", "n8n": { "n8nNodesApiVersion": 1, "nodes": [123] } }',
|
|
149
|
+
errors: [{ messageId: 'nodePathNotString' }],
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: 'multiple bad node paths each report',
|
|
153
|
+
filename: 'package.json',
|
|
154
|
+
code: '{ "name": "n8n-nodes-example", "n8n": { "n8nNodesApiVersion": 1, "nodes": ["dist/ok.js", "bad/one.js", "./dist/two.js"] } }',
|
|
155
|
+
errors: [
|
|
156
|
+
{ messageId: 'nodePathNotInDist', data: { path: 'bad/one.js' } },
|
|
157
|
+
{ messageId: 'nodePathNotInDist', data: { path: './dist/two.js' } },
|
|
158
|
+
],
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: 'credentials is not an array',
|
|
162
|
+
filename: 'package.json',
|
|
163
|
+
code: '{ "name": "n8n-nodes-example", "n8n": { "n8nNodesApiVersion": 1, "nodes": ["dist/x.js"], "credentials": "dist/c.js" } }',
|
|
164
|
+
errors: [{ messageId: 'n8nCredentialsNotArray' }],
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: 'credential path does not start with dist/',
|
|
168
|
+
filename: 'package.json',
|
|
169
|
+
code: '{ "name": "n8n-nodes-example", "n8n": { "n8nNodesApiVersion": 1, "nodes": ["dist/x.js"], "credentials": ["credentials/Foo.credentials.js"] } }',
|
|
170
|
+
errors: [
|
|
171
|
+
{
|
|
172
|
+
messageId: 'credentialPathNotInDist',
|
|
173
|
+
data: { path: 'credentials/Foo.credentials.js' },
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: 'credential path is not a string',
|
|
179
|
+
filename: 'package.json',
|
|
180
|
+
code: '{ "name": "n8n-nodes-example", "n8n": { "n8nNodesApiVersion": 1, "nodes": ["dist/x.js"], "credentials": [true] } }',
|
|
181
|
+
errors: [{ messageId: 'credentialPathNotString' }],
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: 'strict is a string',
|
|
185
|
+
filename: 'package.json',
|
|
186
|
+
code: '{ "name": "n8n-nodes-example", "n8n": { "n8nNodesApiVersion": 1, "nodes": ["dist/x.js"], "strict": "true" } }',
|
|
187
|
+
errors: [{ messageId: 'invalidStrict', data: { value: 'true' } }],
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: 'strict is a number',
|
|
191
|
+
filename: 'package.json',
|
|
192
|
+
code: '{ "name": "n8n-nodes-example", "n8n": { "n8nNodesApiVersion": 1, "nodes": ["dist/x.js"], "strict": 1 } }',
|
|
193
|
+
errors: [{ messageId: 'invalidStrict', data: { value: '1' } }],
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
name: 'strict is null',
|
|
197
|
+
filename: 'package.json',
|
|
198
|
+
code: '{ "name": "n8n-nodes-example", "n8n": { "n8nNodesApiVersion": 1, "nodes": ["dist/x.js"], "strict": null } }',
|
|
199
|
+
errors: [{ messageId: 'invalidStrict', data: { value: 'null' } }],
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
});
|