@n8n/eslint-plugin-community-nodes 0.14.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.
Files changed (83) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/README.md +7 -0
  3. package/dist/plugin.d.ts +48 -0
  4. package/dist/plugin.d.ts.map +1 -1
  5. package/dist/plugin.js +16 -0
  6. package/dist/plugin.js.map +1 -1
  7. package/dist/rules/cred-class-name-suffix.d.ts +2 -0
  8. package/dist/rules/cred-class-name-suffix.d.ts.map +1 -0
  9. package/dist/rules/cred-class-name-suffix.js +53 -0
  10. package/dist/rules/cred-class-name-suffix.js.map +1 -0
  11. package/dist/rules/cred-class-oauth2-naming.d.ts +2 -0
  12. package/dist/rules/cred-class-oauth2-naming.d.ts.map +1 -0
  13. package/dist/rules/cred-class-oauth2-naming.js +96 -0
  14. package/dist/rules/cred-class-oauth2-naming.js.map +1 -0
  15. package/dist/rules/index.d.ts +11 -1
  16. package/dist/rules/index.d.ts.map +1 -1
  17. package/dist/rules/index.js +16 -0
  18. package/dist/rules/index.js.map +1 -1
  19. package/dist/rules/n8n-object-validation.d.ts +5 -0
  20. package/dist/rules/n8n-object-validation.d.ts.map +1 -0
  21. package/dist/rules/n8n-object-validation.js +148 -0
  22. package/dist/rules/n8n-object-validation.js.map +1 -0
  23. package/dist/rules/no-builder-hint-leakage.d.ts +7 -0
  24. package/dist/rules/no-builder-hint-leakage.d.ts.map +1 -0
  25. package/dist/rules/no-builder-hint-leakage.js +99 -0
  26. package/dist/rules/no-builder-hint-leakage.js.map +1 -0
  27. package/dist/rules/no-overrides-field.js +1 -1
  28. package/dist/rules/no-overrides-field.js.map +1 -1
  29. package/dist/rules/no-runtime-dependencies.d.ts +2 -0
  30. package/dist/rules/no-runtime-dependencies.d.ts.map +1 -0
  31. package/dist/rules/no-runtime-dependencies.js +41 -0
  32. package/dist/rules/no-runtime-dependencies.js.map +1 -0
  33. package/dist/rules/no-template-placeholders.d.ts +2 -0
  34. package/dist/rules/no-template-placeholders.d.ts.map +1 -0
  35. package/dist/rules/no-template-placeholders.js +57 -0
  36. package/dist/rules/no-template-placeholders.js.map +1 -0
  37. package/dist/rules/node-operation-error-itemindex.d.ts +12 -0
  38. package/dist/rules/node-operation-error-itemindex.d.ts.map +1 -0
  39. package/dist/rules/node-operation-error-itemindex.js +184 -0
  40. package/dist/rules/node-operation-error-itemindex.js.map +1 -0
  41. package/dist/rules/valid-credential-references.d.ts +2 -0
  42. package/dist/rules/valid-credential-references.d.ts.map +1 -0
  43. package/dist/rules/valid-credential-references.js +77 -0
  44. package/dist/rules/valid-credential-references.js.map +1 -0
  45. package/dist/rules/webhook-lifecycle-complete.d.ts +1 -1
  46. package/dist/rules/webhook-lifecycle-complete.d.ts.map +1 -1
  47. package/dist/rules/webhook-lifecycle-complete.js +8 -0
  48. package/dist/rules/webhook-lifecycle-complete.js.map +1 -1
  49. package/dist/utils/ast-utils.d.ts.map +1 -1
  50. package/dist/utils/ast-utils.js +5 -1
  51. package/dist/utils/ast-utils.js.map +1 -1
  52. package/docs/rules/cred-class-name-suffix.md +46 -0
  53. package/docs/rules/cred-class-oauth2-naming.md +68 -0
  54. package/docs/rules/n8n-object-validation.md +93 -0
  55. package/docs/rules/no-overrides-field.md +5 -5
  56. package/docs/rules/no-runtime-dependencies.md +58 -0
  57. package/docs/rules/no-template-placeholders.md +51 -0
  58. package/docs/rules/node-operation-error-itemindex.md +81 -0
  59. package/docs/rules/valid-credential-references.md +78 -0
  60. package/package.json +3 -3
  61. package/src/plugin.ts +16 -0
  62. package/src/rules/cred-class-name-suffix.test.ts +74 -0
  63. package/src/rules/cred-class-name-suffix.ts +57 -0
  64. package/src/rules/cred-class-oauth2-naming.test.ts +197 -0
  65. package/src/rules/cred-class-oauth2-naming.ts +118 -0
  66. package/src/rules/index.ts +16 -0
  67. package/src/rules/n8n-object-validation.test.ts +202 -0
  68. package/src/rules/n8n-object-validation.ts +200 -0
  69. package/src/rules/no-builder-hint-leakage.test.ts +84 -0
  70. package/src/rules/no-builder-hint-leakage.ts +112 -0
  71. package/src/rules/no-overrides-field.ts +1 -1
  72. package/src/rules/no-runtime-dependencies.test.ts +50 -0
  73. package/src/rules/no-runtime-dependencies.ts +50 -0
  74. package/src/rules/no-template-placeholders.test.ts +135 -0
  75. package/src/rules/no-template-placeholders.ts +68 -0
  76. package/src/rules/node-operation-error-itemindex.test.ts +280 -0
  77. package/src/rules/node-operation-error-itemindex.ts +223 -0
  78. package/src/rules/valid-credential-references.test.ts +230 -0
  79. package/src/rules/valid-credential-references.ts +105 -0
  80. package/src/rules/webhook-lifecycle-complete.test.ts +5 -0
  81. package/src/rules/webhook-lifecycle-complete.ts +10 -0
  82. package/src/utils/ast-utils.ts +5 -1
  83. package/tsconfig.build.tsbuildinfo +1 -1
@@ -0,0 +1,58 @@
1
+ # Disallow non-empty "dependencies" in community node package.json (`@n8n/community-nodes/no-runtime-dependencies`)
2
+
3
+ 💼 This rule is enabled in the following configs: ✅ `recommended`, ☑️ `recommendedWithoutN8nCloudSupport`.
4
+
5
+ <!-- end auto-generated rule header -->
6
+
7
+ ## Rule Details
8
+
9
+ The `dependencies` field in `package.json` declares packages that are installed alongside the node at runtime. In the context of n8n community nodes this is dangerous:
10
+
11
+ - Community nodes run inside the shared n8n runtime alongside all other installed nodes. Any package listed in `dependencies` gets installed into that shared environment and can shadow or conflict with versions already used by n8n or other nodes.
12
+ - Unlike application packages, community nodes should not own their runtime environment. Shared libraries must be declared in `peerDependencies` (so the host runtime supplies them) or bundled at build time into the published artifact.
13
+ - A non-empty `dependencies` section is a strong signal that the package was scaffolded from a generic Node.js template without adapting it to the n8n community node model.
14
+
15
+ ## Examples
16
+
17
+ ### Incorrect
18
+
19
+ ```json
20
+ {
21
+ "name": "n8n-nodes-example",
22
+ "dependencies": {
23
+ "axios": "1.0.0"
24
+ }
25
+ }
26
+ ```
27
+
28
+ ```json
29
+ {
30
+ "name": "n8n-nodes-example",
31
+ "dependencies": {
32
+ "axios": "1.7.0",
33
+ "fast-xml-parser": "4.4.0",
34
+ "minimatch": "9.0.5"
35
+ }
36
+ }
37
+ ```
38
+
39
+ ### Correct
40
+
41
+ ```json
42
+ {
43
+ "name": "n8n-nodes-example",
44
+ "peerDependencies": {
45
+ "n8n-workflow": "*"
46
+ }
47
+ }
48
+ ```
49
+
50
+ ```json
51
+ {
52
+ "name": "n8n-nodes-example",
53
+ "dependencies": {},
54
+ "peerDependencies": {
55
+ "n8n-workflow": "*"
56
+ }
57
+ }
58
+ ```
@@ -0,0 +1,51 @@
1
+ # Disallow unresolved template placeholders in package.json (`@n8n/community-nodes/no-template-placeholders`)
2
+
3
+ 💼 This rule is enabled in the following configs: ✅ `recommended`, ☑️ `recommendedWithoutN8nCloudSupport`.
4
+
5
+ <!-- end auto-generated rule header -->
6
+
7
+ ## Rule Details
8
+
9
+ Community node packages are typically scaffolded from a starter template that contains
10
+ placeholder values such as `<PACKAGE_NAME>`, `<USERNAME>`, or `{{ authorName }}`. When
11
+ these placeholders survive into a published `package.json`, the package metadata is
12
+ broken — the name is invalid, the repository link is dead, etc.
13
+
14
+ This rule scans every string value in `package.json` and reports any value containing
15
+ an unresolved placeholder pattern. It catches:
16
+
17
+ - Angle bracket placeholders: `<...>`
18
+ - Mustache placeholders: `{{...}}`
19
+
20
+ The rule applies to **all** string fields, including custom ones — not just the well-known
21
+ fields like `name`, `description`, `homepage`, or `repository.url`.
22
+
23
+ ## Examples
24
+
25
+ ### Incorrect
26
+
27
+ ```json
28
+ {
29
+ "name": "n8n-nodes-<PACKAGE_NAME>",
30
+ "description": "An n8n community node for {{service}}",
31
+ "homepage": "https://github.com/<USERNAME>/n8n-nodes-example#readme",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/<USERNAME>/n8n-nodes-example.git"
35
+ }
36
+ }
37
+ ```
38
+
39
+ ### Correct
40
+
41
+ ```json
42
+ {
43
+ "name": "n8n-nodes-acme",
44
+ "description": "An n8n community node for the Acme API",
45
+ "homepage": "https://github.com/acme/n8n-nodes-acme#readme",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/acme/n8n-nodes-acme.git"
49
+ }
50
+ }
51
+ ```
@@ -0,0 +1,81 @@
1
+ # Require { itemIndex } in NodeOperationError / NodeApiError options inside item loops (`@n8n/community-nodes/node-operation-error-itemindex`)
2
+
3
+ 💼 This rule is enabled in the following configs: ✅ `recommended`, ☑️ `recommendedWithoutN8nCloudSupport`.
4
+
5
+ <!-- end auto-generated rule header -->
6
+
7
+ ## Rule Details
8
+
9
+ When throwing `NodeOperationError` or `NodeApiError` inside the item-processing loop of an `execute()` method, the options object (third argument) must contain an `itemIndex` property. Without it, n8n cannot associate the error with the specific item that caused it, which breaks per-item error reporting and `continueOnFail` behaviour.
10
+
11
+ The rule only fires inside **item loops** — `for` or `for...of` statements that iterate over the result of `this.getInputData()`. Errors thrown outside such loops (e.g. in webhook handlers, trigger setup, or credential testing helpers) are not flagged.
12
+
13
+ ## Examples
14
+
15
+ ### ❌ Incorrect
16
+
17
+ ```typescript
18
+ export class MyNode implements INodeType {
19
+ description: INodeTypeDescription = { /* ... */ };
20
+
21
+ async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
22
+ const items = this.getInputData();
23
+ const returnData: INodeExecutionData[] = [];
24
+
25
+ for (let i = 0; i < items.length; i++) {
26
+ try {
27
+ // ...
28
+ } catch (error) {
29
+ // Missing { itemIndex } — n8n cannot map this error back to item i
30
+ throw new NodeOperationError(this.getNode(), error);
31
+ }
32
+ }
33
+
34
+ return [returnData];
35
+ }
36
+ }
37
+ ```
38
+
39
+ ### ✅ Correct
40
+
41
+ ```typescript
42
+ export class MyNode implements INodeType {
43
+ description: INodeTypeDescription = { /* ... */ };
44
+
45
+ async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
46
+ const items = this.getInputData();
47
+ const returnData: INodeExecutionData[] = [];
48
+
49
+ for (let i = 0; i < items.length; i++) {
50
+ try {
51
+ // ...
52
+ } catch (error) {
53
+ throw new NodeOperationError(this.getNode(), error, { itemIndex: i });
54
+ }
55
+ }
56
+
57
+ return [returnData];
58
+ }
59
+ }
60
+ ```
61
+
62
+ Using `for...of` with a named loop variable:
63
+
64
+ ```typescript
65
+ async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
66
+ const items = this.getInputData();
67
+ const returnData: INodeExecutionData[] = [];
68
+ let itemIndex = 0;
69
+
70
+ for (const item of items) {
71
+ try {
72
+ // ...
73
+ } catch (error) {
74
+ throw new NodeApiError(this.getNode(), error, { itemIndex });
75
+ }
76
+ itemIndex++;
77
+ }
78
+
79
+ return [returnData];
80
+ }
81
+ ```
@@ -0,0 +1,78 @@
1
+ # Ensure credentials referenced in node descriptions exist as credential classes in the package (`@n8n/community-nodes/valid-credential-references`)
2
+
3
+ 💼 This rule is enabled in the following configs: ✅ `recommended`, ☑️ `recommendedWithoutN8nCloudSupport`.
4
+
5
+ 💡 This rule is manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
6
+
7
+ <!-- end auto-generated rule header -->
8
+
9
+ ## Rule Details
10
+
11
+ For each entry in `description.credentials[]`, this rule verifies that the referenced `name` matches the `name` class field of a credential class declared in the same package (as listed in `package.json` under `n8n.credentials`).
12
+
13
+ This catches typos and broken references. When `cred-class-name-suffix` is also enabled, this rule naturally enforces the naming convention in the common case while still allowing legitimately named credentials such as `httpHeaderAuth` or `webhookAuth`.
14
+
15
+ ## Examples
16
+
17
+ ### ❌ Incorrect
18
+
19
+ ```typescript
20
+ // MyApiCredential.credentials.ts
21
+ export class MyApiCredential implements ICredentialType {
22
+ name = 'myApiCredential';
23
+ // ...
24
+ }
25
+
26
+ // package.json: "n8n": { "credentials": ["dist/credentials/MyApiCredential.credentials.js"] }
27
+
28
+ export class MyNode implements INodeType {
29
+ description: INodeTypeDescription = {
30
+ credentials: [
31
+ {
32
+ name: 'myApiCredentail', // Typo — no credential with this name exists
33
+ required: true,
34
+ },
35
+ ],
36
+ // ...
37
+ };
38
+ }
39
+ ```
40
+
41
+ ### ✅ Correct
42
+
43
+ ```typescript
44
+ // MyApiCredential.credentials.ts
45
+ export class MyApiCredential implements ICredentialType {
46
+ name = 'myApiCredential';
47
+ // ...
48
+ }
49
+
50
+ // package.json: "n8n": { "credentials": ["dist/credentials/MyApiCredential.credentials.js"] }
51
+
52
+ export class MyNode implements INodeType {
53
+ description: INodeTypeDescription = {
54
+ credentials: [
55
+ {
56
+ name: 'myApiCredential', // Matches the credential class name property
57
+ required: true,
58
+ },
59
+ ],
60
+ // ...
61
+ };
62
+ }
63
+ ```
64
+
65
+ ## Setup
66
+
67
+ Declare your credential files in `package.json` so the rule can resolve credential class names:
68
+
69
+ ```json
70
+ {
71
+ "name": "n8n-nodes-my-service",
72
+ "n8n": {
73
+ "credentials": [
74
+ "dist/credentials/MyApiCredential.credentials.js"
75
+ ]
76
+ }
77
+ }
78
+ ```
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@n8n/eslint-plugin-community-nodes",
3
3
  "type": "module",
4
- "version": "0.14.0",
4
+ "version": "0.16.0",
5
5
  "main": "./dist/plugin.js",
6
6
  "types": "./dist/plugin.d.ts",
7
7
  "exports": {
@@ -23,8 +23,8 @@
23
23
  "typescript": "6.0.2",
24
24
  "vitest": "^4.1.1",
25
25
  "@n8n/typescript-config": "1.4.0",
26
- "n8n-workflow": "2.19.0",
27
- "@n8n/vitest-config": "1.10.0"
26
+ "@n8n/vitest-config": "1.10.0",
27
+ "n8n-workflow": "2.21.0"
28
28
  },
29
29
  "peerDependencies": {
30
30
  "eslint": ">= 9",
package/src/plugin.ts CHANGED
@@ -24,6 +24,7 @@ const configs = {
24
24
  '@n8n/community-nodes/no-restricted-globals': 'error',
25
25
  '@n8n/community-nodes/no-restricted-imports': 'error',
26
26
  '@n8n/community-nodes/credential-password-field': 'error',
27
+ '@n8n/community-nodes/n8n-object-validation': 'error',
27
28
  '@n8n/community-nodes/no-deprecated-workflow-functions': 'error',
28
29
  '@n8n/community-nodes/node-usable-as-tool': 'error',
29
30
  '@n8n/community-nodes/package-name-convention': 'error',
@@ -32,17 +33,24 @@ const configs = {
32
33
  '@n8n/community-nodes/no-forbidden-lifecycle-scripts': 'error',
33
34
  '@n8n/community-nodes/no-http-request-with-manual-auth': 'error',
34
35
  '@n8n/community-nodes/no-overrides-field': 'error',
36
+ '@n8n/community-nodes/no-runtime-dependencies': 'error',
37
+ '@n8n/community-nodes/no-template-placeholders': 'error',
35
38
  '@n8n/community-nodes/icon-validation': 'error',
36
39
  '@n8n/community-nodes/options-sorted-alphabetically': 'warn',
37
40
  '@n8n/community-nodes/resource-operation-pattern': 'warn',
38
41
  '@n8n/community-nodes/credential-documentation-url': 'error',
39
42
  '@n8n/community-nodes/cred-class-field-icon-missing': 'error',
43
+ '@n8n/community-nodes/cred-class-name-suffix': 'error',
44
+ '@n8n/community-nodes/cred-class-oauth2-naming': 'error',
40
45
  '@n8n/community-nodes/node-connection-type-literal': 'error',
41
46
  '@n8n/community-nodes/missing-paired-item': 'error',
47
+ '@n8n/community-nodes/no-builder-hint-leakage': 'error',
48
+ '@n8n/community-nodes/node-operation-error-itemindex': 'error',
42
49
  '@n8n/community-nodes/require-community-node-keyword': 'warn',
43
50
  '@n8n/community-nodes/require-continue-on-fail': 'error',
44
51
  '@n8n/community-nodes/require-node-api-error': 'error',
45
52
  '@n8n/community-nodes/require-node-description-fields': 'error',
53
+ '@n8n/community-nodes/valid-credential-references': 'error',
46
54
  '@n8n/community-nodes/valid-peer-dependencies': 'error',
47
55
  '@n8n/community-nodes/webhook-lifecycle-complete': 'error',
48
56
  },
@@ -55,6 +63,7 @@ const configs = {
55
63
  rules: {
56
64
  '@n8n/community-nodes/ai-node-package-json': 'error',
57
65
  '@n8n/community-nodes/credential-password-field': 'error',
66
+ '@n8n/community-nodes/n8n-object-validation': 'error',
58
67
  '@n8n/community-nodes/no-deprecated-workflow-functions': 'error',
59
68
  '@n8n/community-nodes/node-usable-as-tool': 'error',
60
69
  '@n8n/community-nodes/package-name-convention': 'error',
@@ -63,17 +72,24 @@ const configs = {
63
72
  '@n8n/community-nodes/no-forbidden-lifecycle-scripts': 'error',
64
73
  '@n8n/community-nodes/no-http-request-with-manual-auth': 'error',
65
74
  '@n8n/community-nodes/no-overrides-field': 'error',
75
+ '@n8n/community-nodes/no-runtime-dependencies': 'error',
76
+ '@n8n/community-nodes/no-template-placeholders': 'error',
66
77
  '@n8n/community-nodes/icon-validation': 'error',
67
78
  '@n8n/community-nodes/options-sorted-alphabetically': 'warn',
68
79
  '@n8n/community-nodes/credential-documentation-url': 'error',
69
80
  '@n8n/community-nodes/resource-operation-pattern': 'warn',
70
81
  '@n8n/community-nodes/cred-class-field-icon-missing': 'error',
82
+ '@n8n/community-nodes/cred-class-name-suffix': 'error',
83
+ '@n8n/community-nodes/cred-class-oauth2-naming': 'error',
71
84
  '@n8n/community-nodes/node-connection-type-literal': 'error',
72
85
  '@n8n/community-nodes/missing-paired-item': 'error',
86
+ '@n8n/community-nodes/no-builder-hint-leakage': 'error',
87
+ '@n8n/community-nodes/node-operation-error-itemindex': 'error',
73
88
  '@n8n/community-nodes/require-community-node-keyword': 'warn',
74
89
  '@n8n/community-nodes/require-continue-on-fail': 'error',
75
90
  '@n8n/community-nodes/require-node-api-error': 'error',
76
91
  '@n8n/community-nodes/require-node-description-fields': 'error',
92
+ '@n8n/community-nodes/valid-credential-references': 'error',
77
93
  '@n8n/community-nodes/valid-peer-dependencies': 'error',
78
94
  '@n8n/community-nodes/webhook-lifecycle-complete': 'error',
79
95
  },
@@ -0,0 +1,74 @@
1
+ import { RuleTester } from '@typescript-eslint/rule-tester';
2
+
3
+ import { CredClassNameSuffixRule } from './cred-class-name-suffix.js';
4
+
5
+ const ruleTester = new RuleTester();
6
+
7
+ const credFilePath = '/tmp/TestCredential.credentials.ts';
8
+ const nonCredFilePath = '/tmp/SomeHelper.ts';
9
+
10
+ function createCredentialCode(className: string): string {
11
+ return `
12
+ import type { ICredentialType, INodeProperties } from 'n8n-workflow';
13
+
14
+ export class ${className} implements ICredentialType {
15
+ name = 'testApi';
16
+ displayName = 'Test API';
17
+ properties: INodeProperties[] = [];
18
+ }`;
19
+ }
20
+
21
+ function createRegularClass(className: string): string {
22
+ return `
23
+ export class ${className} {
24
+ name = 'test';
25
+ }`;
26
+ }
27
+
28
+ ruleTester.run('cred-class-name-suffix', CredClassNameSuffixRule, {
29
+ valid: [
30
+ {
31
+ name: 'credential class with Api suffix',
32
+ filename: credFilePath,
33
+ code: createCredentialCode('TestApi'),
34
+ },
35
+ {
36
+ name: 'credential class with OAuth2Api suffix',
37
+ filename: credFilePath,
38
+ code: createCredentialCode('TestOAuth2Api'),
39
+ },
40
+ {
41
+ name: 'class not implementing ICredentialType is ignored',
42
+ filename: credFilePath,
43
+ code: createRegularClass('SomeHelper'),
44
+ },
45
+ {
46
+ name: 'non-.credentials.ts file is ignored',
47
+ filename: nonCredFilePath,
48
+ code: createCredentialCode('TestCredential'),
49
+ },
50
+ ],
51
+ invalid: [
52
+ {
53
+ name: 'credential class missing Api suffix',
54
+ filename: credFilePath,
55
+ code: createCredentialCode('TestCredential'),
56
+ errors: [{ messageId: 'missingSuffix', data: { name: 'TestCredential' } }],
57
+ output: createCredentialCode('TestCredentialApi'),
58
+ },
59
+ {
60
+ name: 'credential class name ending in Ap',
61
+ filename: credFilePath,
62
+ code: createCredentialCode('TestAp'),
63
+ errors: [{ messageId: 'missingSuffix', data: { name: 'TestAp' } }],
64
+ output: createCredentialCode('TestApi'),
65
+ },
66
+ {
67
+ name: 'credential class name ending in A',
68
+ filename: credFilePath,
69
+ code: createCredentialCode('TestA'),
70
+ errors: [{ messageId: 'missingSuffix', data: { name: 'TestA' } }],
71
+ output: createCredentialCode('TestApi'),
72
+ },
73
+ ],
74
+ });
@@ -0,0 +1,57 @@
1
+ import { isCredentialTypeClass, isFileType, createRule } from '../utils/index.js';
2
+
3
+ function addApiSuffix(name: string): string {
4
+ if (name.endsWith('Ap')) return `${name}i`;
5
+ if (name.endsWith('A')) return `${name}pi`;
6
+ return `${name}Api`;
7
+ }
8
+
9
+ export const CredClassNameSuffixRule = createRule({
10
+ name: 'cred-class-name-suffix',
11
+ meta: {
12
+ type: 'problem',
13
+ docs: {
14
+ description: 'Credential class names must be suffixed with `Api`',
15
+ },
16
+ messages: {
17
+ missingSuffix: "Credential class name '{{name}}' must end with 'Api'",
18
+ },
19
+ fixable: 'code',
20
+ schema: [],
21
+ },
22
+ defaultOptions: [],
23
+ create(context) {
24
+ if (!isFileType(context.filename, '.credentials.ts')) {
25
+ return {};
26
+ }
27
+
28
+ return {
29
+ ClassDeclaration(node) {
30
+ if (!isCredentialTypeClass(node)) {
31
+ return;
32
+ }
33
+
34
+ const classNameNode = node.id;
35
+ if (!classNameNode) {
36
+ return;
37
+ }
38
+
39
+ const className = classNameNode.name;
40
+ if (className.endsWith('Api')) {
41
+ return;
42
+ }
43
+
44
+ const fixedName = addApiSuffix(className);
45
+
46
+ context.report({
47
+ node: classNameNode,
48
+ messageId: 'missingSuffix',
49
+ data: { name: className },
50
+ fix(fixer) {
51
+ return fixer.replaceText(classNameNode, fixedName);
52
+ },
53
+ });
54
+ },
55
+ };
56
+ },
57
+ });
@@ -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
+ });