@n8n/eslint-plugin-community-nodes 0.3.0 → 0.5.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 +2 -2
- package/README.md +60 -0
- package/dist/plugin.d.ts +144 -28
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +28 -25
- package/dist/plugin.js.map +1 -1
- package/dist/rules/credential-documentation-url.d.ts +7 -0
- package/dist/rules/credential-documentation-url.d.ts.map +1 -0
- package/dist/rules/credential-documentation-url.js +100 -0
- package/dist/rules/credential-documentation-url.js.map +1 -0
- package/dist/rules/credential-password-field.d.ts +1 -2
- package/dist/rules/credential-password-field.d.ts.map +1 -1
- package/dist/rules/credential-password-field.js +25 -14
- package/dist/rules/credential-password-field.js.map +1 -1
- package/dist/rules/credential-test-required.d.ts +1 -2
- package/dist/rules/credential-test-required.d.ts.map +1 -1
- package/dist/rules/credential-test-required.js +43 -5
- package/dist/rules/credential-test-required.js.map +1 -1
- package/dist/rules/icon-validation.d.ts +1 -2
- package/dist/rules/icon-validation.d.ts.map +1 -1
- package/dist/rules/icon-validation.js +81 -15
- package/dist/rules/icon-validation.js.map +1 -1
- package/dist/rules/index.d.ts +9 -5
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +7 -5
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/no-credential-reuse.d.ts +1 -2
- package/dist/rules/no-credential-reuse.d.ts.map +1 -1
- package/dist/rules/no-credential-reuse.js +33 -4
- package/dist/rules/no-credential-reuse.js.map +1 -1
- package/dist/rules/no-deprecated-workflow-functions.d.ts +1 -2
- package/dist/rules/no-deprecated-workflow-functions.d.ts.map +1 -1
- package/dist/rules/no-deprecated-workflow-functions.js +38 -10
- package/dist/rules/no-deprecated-workflow-functions.js.map +1 -1
- package/dist/rules/no-restricted-globals.d.ts +2 -2
- package/dist/rules/no-restricted-globals.d.ts.map +1 -1
- package/dist/rules/no-restricted-globals.js +5 -3
- package/dist/rules/no-restricted-globals.js.map +1 -1
- package/dist/rules/no-restricted-imports.d.ts +1 -2
- package/dist/rules/no-restricted-imports.d.ts.map +1 -1
- package/dist/rules/no-restricted-imports.js +3 -3
- package/dist/rules/no-restricted-imports.js.map +1 -1
- package/dist/rules/node-usable-as-tool.d.ts +1 -2
- package/dist/rules/node-usable-as-tool.d.ts.map +1 -1
- package/dist/rules/node-usable-as-tool.js +6 -5
- package/dist/rules/node-usable-as-tool.js.map +1 -1
- package/dist/rules/package-name-convention.d.ts +1 -2
- package/dist/rules/package-name-convention.d.ts.map +1 -1
- package/dist/rules/package-name-convention.js +38 -2
- package/dist/rules/package-name-convention.js.map +1 -1
- package/dist/rules/resource-operation-pattern.d.ts +1 -2
- package/dist/rules/resource-operation-pattern.d.ts.map +1 -1
- package/dist/rules/resource-operation-pattern.js +9 -7
- package/dist/rules/resource-operation-pattern.js.map +1 -1
- package/dist/utils/ast-utils.d.ts +2 -1
- package/dist/utils/ast-utils.d.ts.map +1 -1
- package/dist/utils/ast-utils.js +37 -19
- package/dist/utils/ast-utils.js.map +1 -1
- package/dist/utils/file-utils.d.ts +14 -0
- package/dist/utils/file-utils.d.ts.map +1 -1
- package/dist/utils/file-utils.js +85 -18
- package/dist/utils/file-utils.js.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/rule-creator.d.ts +3 -0
- package/dist/utils/rule-creator.d.ts.map +1 -0
- package/dist/utils/rule-creator.js +5 -0
- package/dist/utils/rule-creator.js.map +1 -0
- package/docs/rules/credential-documentation-url.md +94 -0
- package/docs/rules/credential-password-field.md +45 -0
- package/docs/rules/credential-test-required.md +58 -0
- package/docs/rules/icon-validation.md +67 -0
- package/docs/rules/no-credential-reuse.md +82 -0
- package/docs/rules/no-deprecated-workflow-functions.md +61 -0
- package/docs/rules/no-restricted-globals.md +44 -0
- package/docs/rules/no-restricted-imports.md +47 -0
- package/docs/rules/node-usable-as-tool.md +43 -0
- package/docs/rules/package-name-convention.md +52 -0
- package/docs/rules/resource-operation-pattern.md +84 -0
- package/eslint.config.mjs +27 -0
- package/package.json +25 -4
- package/src/plugin.ts +30 -26
- package/src/rules/credential-documentation-url.test.ts +306 -0
- package/src/rules/credential-documentation-url.ts +129 -0
- package/src/rules/credential-password-field.test.ts +1 -0
- package/src/rules/credential-password-field.ts +34 -16
- package/src/rules/credential-test-required.test.ts +84 -57
- package/src/rules/credential-test-required.ts +51 -5
- package/src/rules/icon-validation.test.ts +97 -14
- package/src/rules/icon-validation.ts +95 -14
- package/src/rules/index.ts +8 -5
- package/src/rules/no-credential-reuse.test.ts +306 -58
- package/src/rules/no-credential-reuse.ts +43 -3
- package/src/rules/no-deprecated-workflow-functions.test.ts +70 -0
- package/src/rules/no-deprecated-workflow-functions.ts +44 -10
- package/src/rules/no-restricted-globals.test.ts +1 -0
- package/src/rules/no-restricted-globals.ts +6 -3
- package/src/rules/no-restricted-imports.test.ts +1 -0
- package/src/rules/no-restricted-imports.ts +8 -3
- package/src/rules/node-usable-as-tool.test.ts +1 -0
- package/src/rules/node-usable-as-tool.ts +8 -6
- package/src/rules/package-name-convention.test.ts +82 -5
- package/src/rules/package-name-convention.ts +46 -2
- package/src/rules/resource-operation-pattern.test.ts +1 -0
- package/src/rules/resource-operation-pattern.ts +13 -6
- package/src/utils/ast-utils.ts +47 -19
- package/src/utils/file-utils.ts +108 -18
- package/src/utils/index.ts +1 -0
- package/src/utils/rule-creator.ts +6 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.eslint.json +5 -0
- package/tsconfig.json +1 -2
|
@@ -1,35 +1,27 @@
|
|
|
1
1
|
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
-
import { NoCredentialReuseRule } from './no-credential-reuse.js';
|
|
3
2
|
import { vi } from 'vitest';
|
|
4
|
-
import * as fs from 'node:fs';
|
|
5
3
|
|
|
6
|
-
|
|
4
|
+
import { NoCredentialReuseRule } from './no-credential-reuse.js';
|
|
5
|
+
import * as fileUtils from '../utils/file-utils.js';
|
|
6
|
+
|
|
7
|
+
vi.mock('../utils/file-utils.js', async () => {
|
|
8
|
+
const actual = await vi.importActual('../utils/file-utils.js');
|
|
9
|
+
return {
|
|
10
|
+
...actual,
|
|
11
|
+
readPackageJsonCredentials: vi.fn(),
|
|
12
|
+
findPackageJson: vi.fn(),
|
|
13
|
+
};
|
|
14
|
+
});
|
|
7
15
|
|
|
8
|
-
|
|
9
|
-
vi.
|
|
10
|
-
readFileSync: vi.fn(),
|
|
11
|
-
existsSync: vi.fn(),
|
|
12
|
-
}));
|
|
16
|
+
const mockReadPackageJsonCredentials = vi.mocked(fileUtils.readPackageJsonCredentials);
|
|
17
|
+
const mockFindPackageJson = vi.mocked(fileUtils.findPackageJson);
|
|
13
18
|
|
|
14
|
-
const
|
|
15
|
-
const mockExistsSync = vi.mocked(fs.existsSync);
|
|
19
|
+
const ruleTester = new RuleTester();
|
|
16
20
|
|
|
17
21
|
const nodeFilePath = '/tmp/TestNode.node.ts';
|
|
18
22
|
|
|
19
|
-
function createCredentialClass(name: string, displayName: string): string {
|
|
20
|
-
return `
|
|
21
|
-
import type { ICredentialType, INodeProperties } from 'n8n-workflow';
|
|
22
|
-
|
|
23
|
-
export class ${name.charAt(0).toUpperCase() + name.slice(1)} implements ICredentialType {
|
|
24
|
-
name = '${name}';
|
|
25
|
-
displayName = '${displayName}';
|
|
26
|
-
properties: INodeProperties[] = [];
|
|
27
|
-
}
|
|
28
|
-
`;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
23
|
function createNodeCode(
|
|
32
|
-
credentials:
|
|
24
|
+
credentials: Array<string | { name: string; required?: boolean }> = [],
|
|
33
25
|
): string {
|
|
34
26
|
const credentialsArray =
|
|
35
27
|
credentials.length > 0
|
|
@@ -66,6 +58,45 @@ export class TestNode implements INodeType {
|
|
|
66
58
|
}`;
|
|
67
59
|
}
|
|
68
60
|
|
|
61
|
+
// Helper function to create expected outputs with double quotes (matching rule fix behavior)
|
|
62
|
+
function createExpectedNodeCode(
|
|
63
|
+
credentials: Array<string | { name: string; required?: boolean }> = [],
|
|
64
|
+
): string {
|
|
65
|
+
const credentialsArray =
|
|
66
|
+
credentials.length > 0
|
|
67
|
+
? credentials
|
|
68
|
+
.map((cred) => {
|
|
69
|
+
if (typeof cred === 'string') {
|
|
70
|
+
return `"${cred}"`;
|
|
71
|
+
} else {
|
|
72
|
+
const required =
|
|
73
|
+
cred.required !== undefined ? `,\n\t\t\t\trequired: ${cred.required}` : '';
|
|
74
|
+
return `{\n\t\t\t\tname: "${cred.name}"${required},\n\t\t\t}`;
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
.join(',\n\t\t\t')
|
|
78
|
+
: '';
|
|
79
|
+
|
|
80
|
+
const credentialsProperty =
|
|
81
|
+
credentials.length > 0 ? `credentials: [\n\t\t\t${credentialsArray}\n\t\t],` : '';
|
|
82
|
+
|
|
83
|
+
return `
|
|
84
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
85
|
+
|
|
86
|
+
export class TestNode implements INodeType {
|
|
87
|
+
description: INodeTypeDescription = {
|
|
88
|
+
displayName: 'Test Node',
|
|
89
|
+
name: 'testNode',
|
|
90
|
+
group: ['output'],
|
|
91
|
+
version: 1,
|
|
92
|
+
inputs: ['main'],
|
|
93
|
+
outputs: ['main'],
|
|
94
|
+
${credentialsProperty}
|
|
95
|
+
properties: [],
|
|
96
|
+
};
|
|
97
|
+
}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
69
100
|
// Helper function to create non-node class
|
|
70
101
|
function createNonNodeClass(): string {
|
|
71
102
|
return `
|
|
@@ -90,41 +121,10 @@ export class NotANode {
|
|
|
90
121
|
}
|
|
91
122
|
|
|
92
123
|
function setupMockFileSystem() {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
'dist/credentials/MyApi.credentials.js',
|
|
98
|
-
'dist/credentials/AnotherApi.credentials.js',
|
|
99
|
-
],
|
|
100
|
-
},
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
const myApiCredential = createCredentialClass('myApiCredential', 'My API');
|
|
104
|
-
const anotherApiCredential = createCredentialClass('anotherApiCredential', 'Another API');
|
|
105
|
-
|
|
106
|
-
mockExistsSync.mockImplementation((path: fs.PathLike) => {
|
|
107
|
-
const pathStr = path.toString();
|
|
108
|
-
return (
|
|
109
|
-
pathStr.includes('package.json') ||
|
|
110
|
-
pathStr.includes('MyApi.credentials.ts') ||
|
|
111
|
-
pathStr.includes('AnotherApi.credentials.ts')
|
|
112
|
-
);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
mockReadFileSync.mockImplementation((path: any): string => {
|
|
116
|
-
const pathStr = path.toString();
|
|
117
|
-
if (pathStr.includes('package.json')) {
|
|
118
|
-
return JSON.stringify(packageJson, null, 2);
|
|
119
|
-
}
|
|
120
|
-
if (pathStr.includes('MyApi.credentials.ts')) {
|
|
121
|
-
return myApiCredential;
|
|
122
|
-
}
|
|
123
|
-
if (pathStr.includes('AnotherApi.credentials.ts')) {
|
|
124
|
-
return anotherApiCredential;
|
|
125
|
-
}
|
|
126
|
-
throw new Error(`File not found: ${pathStr}`);
|
|
127
|
-
});
|
|
124
|
+
mockFindPackageJson.mockReturnValue('/tmp/package.json');
|
|
125
|
+
mockReadPackageJsonCredentials.mockReturnValue(
|
|
126
|
+
new Set(['myApiCredential', 'anotherApiCredential']),
|
|
127
|
+
);
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
setupMockFileSystem();
|
|
@@ -171,6 +171,18 @@ ruleTester.run('no-credential-reuse', NoCredentialReuseRule, {
|
|
|
171
171
|
{
|
|
172
172
|
messageId: 'credentialNotInPackage',
|
|
173
173
|
data: { credentialName: 'ExternalApi' },
|
|
174
|
+
suggestions: [
|
|
175
|
+
{
|
|
176
|
+
messageId: 'useAvailable',
|
|
177
|
+
data: { suggestedName: 'myApiCredential' },
|
|
178
|
+
output: createExpectedNodeCode([{ name: 'myApiCredential', required: true }]),
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
messageId: 'useAvailable',
|
|
182
|
+
data: { suggestedName: 'anotherApiCredential' },
|
|
183
|
+
output: createExpectedNodeCode([{ name: 'anotherApiCredential', required: true }]),
|
|
184
|
+
},
|
|
185
|
+
],
|
|
174
186
|
},
|
|
175
187
|
],
|
|
176
188
|
},
|
|
@@ -182,6 +194,18 @@ ruleTester.run('no-credential-reuse', NoCredentialReuseRule, {
|
|
|
182
194
|
{
|
|
183
195
|
messageId: 'credentialNotInPackage',
|
|
184
196
|
data: { credentialName: 'ExternalApi' },
|
|
197
|
+
suggestions: [
|
|
198
|
+
{
|
|
199
|
+
messageId: 'useAvailable',
|
|
200
|
+
data: { suggestedName: 'myApiCredential' },
|
|
201
|
+
output: createExpectedNodeCode(['myApiCredential']),
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
messageId: 'useAvailable',
|
|
205
|
+
data: { suggestedName: 'anotherApiCredential' },
|
|
206
|
+
output: createExpectedNodeCode(['anotherApiCredential']),
|
|
207
|
+
},
|
|
208
|
+
],
|
|
185
209
|
},
|
|
186
210
|
],
|
|
187
211
|
},
|
|
@@ -197,10 +221,118 @@ ruleTester.run('no-credential-reuse', NoCredentialReuseRule, {
|
|
|
197
221
|
{
|
|
198
222
|
messageId: 'credentialNotInPackage',
|
|
199
223
|
data: { credentialName: 'ExternalApi' },
|
|
224
|
+
suggestions: [
|
|
225
|
+
{
|
|
226
|
+
messageId: 'useAvailable',
|
|
227
|
+
data: { suggestedName: 'myApiCredential' },
|
|
228
|
+
output: `
|
|
229
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
230
|
+
|
|
231
|
+
export class TestNode implements INodeType {
|
|
232
|
+
description: INodeTypeDescription = {
|
|
233
|
+
displayName: 'Test Node',
|
|
234
|
+
name: 'testNode',
|
|
235
|
+
group: ['output'],
|
|
236
|
+
version: 1,
|
|
237
|
+
inputs: ['main'],
|
|
238
|
+
outputs: ['main'],
|
|
239
|
+
credentials: [
|
|
240
|
+
'myApiCredential',
|
|
241
|
+
{
|
|
242
|
+
name: "myApiCredential",
|
|
243
|
+
required: true,
|
|
244
|
+
},
|
|
245
|
+
'AnotherExternalApi'
|
|
246
|
+
],
|
|
247
|
+
properties: [],
|
|
248
|
+
};
|
|
249
|
+
}`,
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
messageId: 'useAvailable',
|
|
253
|
+
data: { suggestedName: 'anotherApiCredential' },
|
|
254
|
+
output: `
|
|
255
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
256
|
+
|
|
257
|
+
export class TestNode implements INodeType {
|
|
258
|
+
description: INodeTypeDescription = {
|
|
259
|
+
displayName: 'Test Node',
|
|
260
|
+
name: 'testNode',
|
|
261
|
+
group: ['output'],
|
|
262
|
+
version: 1,
|
|
263
|
+
inputs: ['main'],
|
|
264
|
+
outputs: ['main'],
|
|
265
|
+
credentials: [
|
|
266
|
+
'myApiCredential',
|
|
267
|
+
{
|
|
268
|
+
name: "anotherApiCredential",
|
|
269
|
+
required: true,
|
|
270
|
+
},
|
|
271
|
+
'AnotherExternalApi'
|
|
272
|
+
],
|
|
273
|
+
properties: [],
|
|
274
|
+
};
|
|
275
|
+
}`,
|
|
276
|
+
},
|
|
277
|
+
],
|
|
200
278
|
},
|
|
201
279
|
{
|
|
202
280
|
messageId: 'credentialNotInPackage',
|
|
203
281
|
data: { credentialName: 'AnotherExternalApi' },
|
|
282
|
+
suggestions: [
|
|
283
|
+
{
|
|
284
|
+
messageId: 'useAvailable',
|
|
285
|
+
data: { suggestedName: 'myApiCredential' },
|
|
286
|
+
output: `
|
|
287
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
288
|
+
|
|
289
|
+
export class TestNode implements INodeType {
|
|
290
|
+
description: INodeTypeDescription = {
|
|
291
|
+
displayName: 'Test Node',
|
|
292
|
+
name: 'testNode',
|
|
293
|
+
group: ['output'],
|
|
294
|
+
version: 1,
|
|
295
|
+
inputs: ['main'],
|
|
296
|
+
outputs: ['main'],
|
|
297
|
+
credentials: [
|
|
298
|
+
'myApiCredential',
|
|
299
|
+
{
|
|
300
|
+
name: 'ExternalApi',
|
|
301
|
+
required: true,
|
|
302
|
+
},
|
|
303
|
+
"myApiCredential"
|
|
304
|
+
],
|
|
305
|
+
properties: [],
|
|
306
|
+
};
|
|
307
|
+
}`,
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
messageId: 'useAvailable',
|
|
311
|
+
data: { suggestedName: 'anotherApiCredential' },
|
|
312
|
+
output: `
|
|
313
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
314
|
+
|
|
315
|
+
export class TestNode implements INodeType {
|
|
316
|
+
description: INodeTypeDescription = {
|
|
317
|
+
displayName: 'Test Node',
|
|
318
|
+
name: 'testNode',
|
|
319
|
+
group: ['output'],
|
|
320
|
+
version: 1,
|
|
321
|
+
inputs: ['main'],
|
|
322
|
+
outputs: ['main'],
|
|
323
|
+
credentials: [
|
|
324
|
+
'myApiCredential',
|
|
325
|
+
{
|
|
326
|
+
name: 'ExternalApi',
|
|
327
|
+
required: true,
|
|
328
|
+
},
|
|
329
|
+
"anotherApiCredential"
|
|
330
|
+
],
|
|
331
|
+
properties: [],
|
|
332
|
+
};
|
|
333
|
+
}`,
|
|
334
|
+
},
|
|
335
|
+
],
|
|
204
336
|
},
|
|
205
337
|
],
|
|
206
338
|
},
|
|
@@ -215,10 +347,126 @@ ruleTester.run('no-credential-reuse', NoCredentialReuseRule, {
|
|
|
215
347
|
{
|
|
216
348
|
messageId: 'credentialNotInPackage',
|
|
217
349
|
data: { credentialName: 'ExternalApi1' },
|
|
350
|
+
suggestions: [
|
|
351
|
+
{
|
|
352
|
+
messageId: 'useAvailable',
|
|
353
|
+
data: { suggestedName: 'myApiCredential' },
|
|
354
|
+
output: `
|
|
355
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
356
|
+
|
|
357
|
+
export class TestNode implements INodeType {
|
|
358
|
+
description: INodeTypeDescription = {
|
|
359
|
+
displayName: 'Test Node',
|
|
360
|
+
name: 'testNode',
|
|
361
|
+
group: ['output'],
|
|
362
|
+
version: 1,
|
|
363
|
+
inputs: ['main'],
|
|
364
|
+
outputs: ['main'],
|
|
365
|
+
credentials: [
|
|
366
|
+
{
|
|
367
|
+
name: "myApiCredential",
|
|
368
|
+
required: true,
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
name: 'ExternalApi2',
|
|
372
|
+
required: false,
|
|
373
|
+
}
|
|
374
|
+
],
|
|
375
|
+
properties: [],
|
|
376
|
+
};
|
|
377
|
+
}`,
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
messageId: 'useAvailable',
|
|
381
|
+
data: { suggestedName: 'anotherApiCredential' },
|
|
382
|
+
output: `
|
|
383
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
384
|
+
|
|
385
|
+
export class TestNode implements INodeType {
|
|
386
|
+
description: INodeTypeDescription = {
|
|
387
|
+
displayName: 'Test Node',
|
|
388
|
+
name: 'testNode',
|
|
389
|
+
group: ['output'],
|
|
390
|
+
version: 1,
|
|
391
|
+
inputs: ['main'],
|
|
392
|
+
outputs: ['main'],
|
|
393
|
+
credentials: [
|
|
394
|
+
{
|
|
395
|
+
name: "anotherApiCredential",
|
|
396
|
+
required: true,
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
name: 'ExternalApi2',
|
|
400
|
+
required: false,
|
|
401
|
+
}
|
|
402
|
+
],
|
|
403
|
+
properties: [],
|
|
404
|
+
};
|
|
405
|
+
}`,
|
|
406
|
+
},
|
|
407
|
+
],
|
|
218
408
|
},
|
|
219
409
|
{
|
|
220
410
|
messageId: 'credentialNotInPackage',
|
|
221
411
|
data: { credentialName: 'ExternalApi2' },
|
|
412
|
+
suggestions: [
|
|
413
|
+
{
|
|
414
|
+
messageId: 'useAvailable',
|
|
415
|
+
data: { suggestedName: 'myApiCredential' },
|
|
416
|
+
output: `
|
|
417
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
418
|
+
|
|
419
|
+
export class TestNode implements INodeType {
|
|
420
|
+
description: INodeTypeDescription = {
|
|
421
|
+
displayName: 'Test Node',
|
|
422
|
+
name: 'testNode',
|
|
423
|
+
group: ['output'],
|
|
424
|
+
version: 1,
|
|
425
|
+
inputs: ['main'],
|
|
426
|
+
outputs: ['main'],
|
|
427
|
+
credentials: [
|
|
428
|
+
{
|
|
429
|
+
name: 'ExternalApi1',
|
|
430
|
+
required: true,
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
name: "myApiCredential",
|
|
434
|
+
required: false,
|
|
435
|
+
}
|
|
436
|
+
],
|
|
437
|
+
properties: [],
|
|
438
|
+
};
|
|
439
|
+
}`,
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
messageId: 'useAvailable',
|
|
443
|
+
data: { suggestedName: 'anotherApiCredential' },
|
|
444
|
+
output: `
|
|
445
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
446
|
+
|
|
447
|
+
export class TestNode implements INodeType {
|
|
448
|
+
description: INodeTypeDescription = {
|
|
449
|
+
displayName: 'Test Node',
|
|
450
|
+
name: 'testNode',
|
|
451
|
+
group: ['output'],
|
|
452
|
+
version: 1,
|
|
453
|
+
inputs: ['main'],
|
|
454
|
+
outputs: ['main'],
|
|
455
|
+
credentials: [
|
|
456
|
+
{
|
|
457
|
+
name: 'ExternalApi1',
|
|
458
|
+
required: true,
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
name: "anotherApiCredential",
|
|
462
|
+
required: false,
|
|
463
|
+
}
|
|
464
|
+
],
|
|
465
|
+
properties: [],
|
|
466
|
+
};
|
|
467
|
+
}`,
|
|
468
|
+
},
|
|
469
|
+
],
|
|
222
470
|
},
|
|
223
471
|
],
|
|
224
472
|
},
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TSESTree } from '@typescript-eslint/types';
|
|
2
|
+
import type { ReportSuggestionArray } from '@typescript-eslint/utils/ts-eslint';
|
|
3
|
+
|
|
2
4
|
import {
|
|
3
5
|
isNodeTypeClass,
|
|
4
6
|
findClassProperty,
|
|
@@ -7,9 +9,12 @@ import {
|
|
|
7
9
|
findPackageJson,
|
|
8
10
|
readPackageJsonCredentials,
|
|
9
11
|
isFileType,
|
|
12
|
+
findSimilarStrings,
|
|
13
|
+
createRule,
|
|
10
14
|
} from '../utils/index.js';
|
|
11
15
|
|
|
12
|
-
export const NoCredentialReuseRule =
|
|
16
|
+
export const NoCredentialReuseRule = createRule({
|
|
17
|
+
name: 'no-credential-reuse',
|
|
13
18
|
meta: {
|
|
14
19
|
type: 'problem',
|
|
15
20
|
docs: {
|
|
@@ -17,10 +22,13 @@ export const NoCredentialReuseRule = ESLintUtils.RuleCreator.withoutDocs({
|
|
|
17
22
|
'Prevent credential re-use security issues by ensuring nodes only reference credentials from the same package',
|
|
18
23
|
},
|
|
19
24
|
messages: {
|
|
25
|
+
didYouMean: "Did you mean '{{ suggestedName }}'?",
|
|
26
|
+
useAvailable: "Use available credential '{{ suggestedName }}'",
|
|
20
27
|
credentialNotInPackage:
|
|
21
28
|
'SECURITY: Node references credential "{{ credentialName }}" which is not defined in this package. This creates a security risk as it attempts to reuse credentials from other packages. Nodes can only use credentials from the same package as listed in package.json n8n.credentials field.',
|
|
22
29
|
},
|
|
23
30
|
schema: [],
|
|
31
|
+
hasSuggestions: true,
|
|
24
32
|
},
|
|
25
33
|
defaultOptions: [],
|
|
26
34
|
create(context) {
|
|
@@ -52,7 +60,10 @@ export const NoCredentialReuseRule = ESLintUtils.RuleCreator.withoutDocs({
|
|
|
52
60
|
}
|
|
53
61
|
|
|
54
62
|
const descriptionProperty = findClassProperty(node, 'description');
|
|
55
|
-
if (
|
|
63
|
+
if (
|
|
64
|
+
!descriptionProperty?.value ||
|
|
65
|
+
descriptionProperty.value.type !== TSESTree.AST_NODE_TYPES.ObjectExpression
|
|
66
|
+
) {
|
|
56
67
|
return;
|
|
57
68
|
}
|
|
58
69
|
|
|
@@ -66,12 +77,41 @@ export const NoCredentialReuseRule = ESLintUtils.RuleCreator.withoutDocs({
|
|
|
66
77
|
credentialsArray.elements.forEach((element) => {
|
|
67
78
|
const credentialInfo = extractCredentialNameFromArray(element);
|
|
68
79
|
if (credentialInfo && !allowedCredentials.has(credentialInfo.name)) {
|
|
80
|
+
const similarCredentials = findSimilarStrings(credentialInfo.name, allowedCredentials);
|
|
81
|
+
const suggestions: ReportSuggestionArray<
|
|
82
|
+
'didYouMean' | 'useAvailable' | 'credentialNotInPackage'
|
|
83
|
+
> = [];
|
|
84
|
+
|
|
85
|
+
for (const similarName of similarCredentials) {
|
|
86
|
+
suggestions.push({
|
|
87
|
+
messageId: 'didYouMean',
|
|
88
|
+
data: { suggestedName: similarName },
|
|
89
|
+
fix(fixer) {
|
|
90
|
+
return fixer.replaceText(credentialInfo.node, `"${similarName}"`);
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (suggestions.length === 0 && allowedCredentials.size > 0) {
|
|
96
|
+
const availableCredentials = Array.from(allowedCredentials).slice(0, 3);
|
|
97
|
+
for (const availableName of availableCredentials) {
|
|
98
|
+
suggestions.push({
|
|
99
|
+
messageId: 'useAvailable',
|
|
100
|
+
data: { suggestedName: availableName },
|
|
101
|
+
fix(fixer) {
|
|
102
|
+
return fixer.replaceText(credentialInfo.node, `"${availableName}"`);
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
69
108
|
context.report({
|
|
70
109
|
node: credentialInfo.node,
|
|
71
110
|
messageId: 'credentialNotInPackage',
|
|
72
111
|
data: {
|
|
73
112
|
credentialName: credentialInfo.name,
|
|
74
113
|
},
|
|
114
|
+
suggest: suggestions,
|
|
75
115
|
});
|
|
76
116
|
}
|
|
77
117
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
|
|
2
3
|
import { NoDeprecatedWorkflowFunctionsRule } from './no-deprecated-workflow-functions.js';
|
|
3
4
|
|
|
4
5
|
const ruleTester = new RuleTester();
|
|
@@ -60,6 +61,16 @@ const response3 = await this.helpers.requestOAuth2.call(this, 'google', options)
|
|
|
60
61
|
{
|
|
61
62
|
messageId: 'deprecatedRequestFunction',
|
|
62
63
|
data: { functionName: 'request', replacement: 'httpRequest' },
|
|
64
|
+
suggestions: [
|
|
65
|
+
{
|
|
66
|
+
messageId: 'suggestReplaceFunction',
|
|
67
|
+
data: { functionName: 'request', replacement: 'httpRequest' },
|
|
68
|
+
output: `
|
|
69
|
+
const response1 = await this.helpers.httpRequest('https://example.com/1');
|
|
70
|
+
const response2 = await this.helpers.requestWithAuthentication.call(this, 'oauth', options);
|
|
71
|
+
const response3 = await this.helpers.requestOAuth2.call(this, 'google', options);`,
|
|
72
|
+
},
|
|
73
|
+
],
|
|
63
74
|
},
|
|
64
75
|
{
|
|
65
76
|
messageId: 'deprecatedRequestFunction',
|
|
@@ -67,10 +78,33 @@ const response3 = await this.helpers.requestOAuth2.call(this, 'google', options)
|
|
|
67
78
|
functionName: 'requestWithAuthentication',
|
|
68
79
|
replacement: 'httpRequestWithAuthentication',
|
|
69
80
|
},
|
|
81
|
+
suggestions: [
|
|
82
|
+
{
|
|
83
|
+
messageId: 'suggestReplaceFunction',
|
|
84
|
+
data: {
|
|
85
|
+
functionName: 'requestWithAuthentication',
|
|
86
|
+
replacement: 'httpRequestWithAuthentication',
|
|
87
|
+
},
|
|
88
|
+
output: `
|
|
89
|
+
const response1 = await this.helpers.request('https://example.com/1');
|
|
90
|
+
const response2 = await this.helpers.httpRequestWithAuthentication.call(this, 'oauth', options);
|
|
91
|
+
const response3 = await this.helpers.requestOAuth2.call(this, 'google', options);`,
|
|
92
|
+
},
|
|
93
|
+
],
|
|
70
94
|
},
|
|
71
95
|
{
|
|
72
96
|
messageId: 'deprecatedRequestFunction',
|
|
73
97
|
data: { functionName: 'requestOAuth2', replacement: 'httpRequestWithAuthentication' },
|
|
98
|
+
suggestions: [
|
|
99
|
+
{
|
|
100
|
+
messageId: 'suggestReplaceFunction',
|
|
101
|
+
data: { functionName: 'requestOAuth2', replacement: 'httpRequestWithAuthentication' },
|
|
102
|
+
output: `
|
|
103
|
+
const response1 = await this.helpers.request('https://example.com/1');
|
|
104
|
+
const response2 = await this.helpers.requestWithAuthentication.call(this, 'oauth', options);
|
|
105
|
+
const response3 = await this.helpers.httpRequestWithAuthentication.call(this, 'google', options);`,
|
|
106
|
+
},
|
|
107
|
+
],
|
|
74
108
|
},
|
|
75
109
|
],
|
|
76
110
|
},
|
|
@@ -86,14 +120,50 @@ function makeRequest(options: IRequestOptions): Promise<any> {
|
|
|
86
120
|
{
|
|
87
121
|
messageId: 'deprecatedType',
|
|
88
122
|
data: { typeName: 'IRequestOptions', replacement: 'IHttpRequestOptions' },
|
|
123
|
+
suggestions: [
|
|
124
|
+
{
|
|
125
|
+
messageId: 'suggestReplaceType',
|
|
126
|
+
data: { typeName: 'IRequestOptions', replacement: 'IHttpRequestOptions' },
|
|
127
|
+
output: `
|
|
128
|
+
import { IHttpRequestOptions } from 'n8n-workflow';
|
|
129
|
+
|
|
130
|
+
function makeRequest(options: IRequestOptions): Promise<any> {
|
|
131
|
+
return this.helpers.request(options);
|
|
132
|
+
}`,
|
|
133
|
+
},
|
|
134
|
+
],
|
|
89
135
|
},
|
|
90
136
|
{
|
|
91
137
|
messageId: 'deprecatedType',
|
|
92
138
|
data: { typeName: 'IRequestOptions', replacement: 'IHttpRequestOptions' },
|
|
139
|
+
suggestions: [
|
|
140
|
+
{
|
|
141
|
+
messageId: 'suggestReplaceType',
|
|
142
|
+
data: { typeName: 'IRequestOptions', replacement: 'IHttpRequestOptions' },
|
|
143
|
+
output: `
|
|
144
|
+
import { IRequestOptions } from 'n8n-workflow';
|
|
145
|
+
|
|
146
|
+
function makeRequest(options: IHttpRequestOptions): Promise<any> {
|
|
147
|
+
return this.helpers.request(options);
|
|
148
|
+
}`,
|
|
149
|
+
},
|
|
150
|
+
],
|
|
93
151
|
},
|
|
94
152
|
{
|
|
95
153
|
messageId: 'deprecatedRequestFunction',
|
|
96
154
|
data: { functionName: 'request', replacement: 'httpRequest' },
|
|
155
|
+
suggestions: [
|
|
156
|
+
{
|
|
157
|
+
messageId: 'suggestReplaceFunction',
|
|
158
|
+
data: { functionName: 'request', replacement: 'httpRequest' },
|
|
159
|
+
output: `
|
|
160
|
+
import { IRequestOptions } from 'n8n-workflow';
|
|
161
|
+
|
|
162
|
+
function makeRequest(options: IRequestOptions): Promise<any> {
|
|
163
|
+
return this.helpers.httpRequest(options);
|
|
164
|
+
}`,
|
|
165
|
+
},
|
|
166
|
+
],
|
|
97
167
|
},
|
|
98
168
|
],
|
|
99
169
|
},
|