@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.
Files changed (63) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/README.md +5 -0
  3. package/dist/plugin.d.ts +36 -0
  4. package/dist/plugin.d.ts.map +1 -1
  5. package/dist/plugin.js +12 -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 +8 -0
  16. package/dist/rules/index.d.ts.map +1 -1
  17. package/dist/rules/index.js +12 -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-template-placeholders.d.ts +2 -0
  30. package/dist/rules/no-template-placeholders.d.ts.map +1 -0
  31. package/dist/rules/no-template-placeholders.js +57 -0
  32. package/dist/rules/no-template-placeholders.js.map +1 -0
  33. package/dist/rules/node-operation-error-itemindex.d.ts +12 -0
  34. package/dist/rules/node-operation-error-itemindex.d.ts.map +1 -0
  35. package/dist/rules/node-operation-error-itemindex.js +184 -0
  36. package/dist/rules/node-operation-error-itemindex.js.map +1 -0
  37. package/dist/utils/ast-utils.d.ts.map +1 -1
  38. package/dist/utils/ast-utils.js +5 -1
  39. package/dist/utils/ast-utils.js.map +1 -1
  40. package/docs/rules/cred-class-name-suffix.md +46 -0
  41. package/docs/rules/cred-class-oauth2-naming.md +68 -0
  42. package/docs/rules/n8n-object-validation.md +93 -0
  43. package/docs/rules/no-overrides-field.md +5 -5
  44. package/docs/rules/no-template-placeholders.md +51 -0
  45. package/docs/rules/node-operation-error-itemindex.md +81 -0
  46. package/package.json +2 -2
  47. package/src/plugin.ts +12 -0
  48. package/src/rules/cred-class-name-suffix.test.ts +74 -0
  49. package/src/rules/cred-class-name-suffix.ts +57 -0
  50. package/src/rules/cred-class-oauth2-naming.test.ts +197 -0
  51. package/src/rules/cred-class-oauth2-naming.ts +118 -0
  52. package/src/rules/index.ts +12 -0
  53. package/src/rules/n8n-object-validation.test.ts +202 -0
  54. package/src/rules/n8n-object-validation.ts +200 -0
  55. package/src/rules/no-builder-hint-leakage.test.ts +84 -0
  56. package/src/rules/no-builder-hint-leakage.ts +112 -0
  57. package/src/rules/no-overrides-field.ts +1 -1
  58. package/src/rules/no-template-placeholders.test.ts +135 -0
  59. package/src/rules/no-template-placeholders.ts +68 -0
  60. package/src/rules/node-operation-error-itemindex.test.ts +280 -0
  61. package/src/rules/node-operation-error-itemindex.ts +223 -0
  62. package/src/utils/ast-utils.ts +5 -1
  63. package/tsconfig.build.tsbuildinfo +1 -1
@@ -0,0 +1,46 @@
1
+ # Credential class names must be suffixed with `Api` (`@n8n/community-nodes/cred-class-name-suffix`)
2
+
3
+ 💼 This rule is enabled in the following configs: ✅ `recommended`, ☑️ `recommendedWithoutN8nCloudSupport`.
4
+
5
+ 🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6
+
7
+ <!-- end auto-generated rule header -->
8
+
9
+ ## Rule Details
10
+
11
+ Credential classes (those implementing `ICredentialType` in `*.credentials.ts` files) must have a class name ending in `Api`. This is the n8n convention so credentials are easily recognisable in code, in import statements, and across the credential registry.
12
+
13
+ For OAuth2 credentials extending the OAuth2 base, see the sibling rule `cred-class-oauth2-naming` which enforces the more specific `OAuth2Api` suffix.
14
+
15
+ ## Opt-out
16
+
17
+ For legitimate exceptions (for example credentials representing custom auth headers where the `Api` suffix would be misleading), disable the rule for the specific class with a standard ESLint comment:
18
+
19
+ ```typescript
20
+ // eslint-disable-next-line @n8n/community-nodes/cred-class-name-suffix
21
+ export class CustomAuthHeader implements ICredentialType {
22
+ // ...
23
+ }
24
+ ```
25
+
26
+ ## Examples
27
+
28
+ ### ❌ Incorrect
29
+
30
+ ```typescript
31
+ export class MyService implements ICredentialType {
32
+ name = 'myServiceApi';
33
+ displayName = 'My Service API';
34
+ properties: INodeProperties[] = [];
35
+ }
36
+ ```
37
+
38
+ ### ✅ Correct
39
+
40
+ ```typescript
41
+ export class MyServiceApi implements ICredentialType {
42
+ name = 'myServiceApi';
43
+ displayName = 'My Service API';
44
+ properties: INodeProperties[] = [];
45
+ }
46
+ ```
@@ -0,0 +1,68 @@
1
+ # OAuth2 credentials must include `OAuth2` in the class name, `name`, and `displayName` (`@n8n/community-nodes/cred-class-oauth2-naming`)
2
+
3
+ 💼 This rule is enabled in the following configs: ✅ `recommended`, ☑️ `recommendedWithoutN8nCloudSupport`.
4
+
5
+ 🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6
+
7
+ <!-- end auto-generated rule header -->
8
+
9
+ ## Rule Details
10
+
11
+ OAuth2 credentials must consistently identify themselves as OAuth2 across all three naming surfaces. This makes them easy to distinguish from other auth flavours (API key, basic auth) in code, in import statements, and in the credentials picker.
12
+
13
+ A credential class is considered an OAuth2 credential if **any** of the following are true:
14
+
15
+ - The class name contains `OAuth2` (e.g. `GoogleOAuth2Api`).
16
+ - The class TypeScript-extends a base whose name contains `OAuth2` (e.g. `class GoogleOAuth2Api extends OAuth2Api`).
17
+ - The `extends` class field references an `OAuth2` credential (e.g. `extends = ['oAuth2Api']`).
18
+ - The `name` or `displayName` field contains `OAuth2`.
19
+
20
+ When a credential is detected as OAuth2, all of the following must contain `OAuth2`:
21
+
22
+ - Class name (must end with `OAuth2Api`).
23
+ - `name` class field.
24
+ - `displayName` class field.
25
+
26
+ The class name is auto-fixable; `name` and `displayName` must be updated manually because their casing convention varies.
27
+
28
+ ## Examples
29
+
30
+ ### ❌ Incorrect
31
+
32
+ ```typescript
33
+ // Class name detected as OAuth2 but `name` and `displayName` lack OAuth2.
34
+ export class GoogleOAuth2Api implements ICredentialType {
35
+ name = 'googleApi';
36
+ displayName = 'Google API';
37
+ properties: INodeProperties[] = [];
38
+ }
39
+ ```
40
+
41
+ ```typescript
42
+ // Extends OAuth2 base, but neither class name, `name`, nor `displayName` reflect that.
43
+ export class GoogleApi implements ICredentialType {
44
+ name = 'googleApi';
45
+ displayName = 'Google API';
46
+ extends = ['oAuth2Api'];
47
+ properties: INodeProperties[] = [];
48
+ }
49
+ ```
50
+
51
+ ### ✅ Correct
52
+
53
+ ```typescript
54
+ export class GoogleOAuth2Api implements ICredentialType {
55
+ name = 'googleOAuth2Api';
56
+ displayName = 'Google OAuth2 API';
57
+ extends = ['oAuth2Api'];
58
+ properties: INodeProperties[] = [];
59
+ }
60
+ ```
61
+
62
+ ## Migrated from
63
+
64
+ This rule consolidates three rules from the legacy `eslint-plugin-n8n-nodes-base` plugin into a single conceptual check:
65
+
66
+ - `cred-class-name-missing-oauth2-suffix`
67
+ - `cred-class-field-name-missing-oauth2`
68
+ - `cred-class-field-display-name-missing-oauth2`
@@ -0,0 +1,93 @@
1
+ # Validate the structure of the "n8n" object in community node package.json (required keys, types, and dist/ paths) (`@n8n/community-nodes/n8n-object-validation`)
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
+ Every community node package declares its nodes and credentials inside the
10
+ top-level `n8n` object in `package.json`. n8n loads packages by reading this
11
+ object: it looks up `n8n.n8nNodesApiVersion` to pick the correct loader, and it
12
+ loads each path in `n8n.nodes` and `n8n.credentials` as compiled JavaScript
13
+ relative to the package root. If any of those fields are missing, mistyped, or
14
+ point at TypeScript sources instead of compiled output, the package fails to
15
+ register at install time and the failure is opaque to the user.
16
+
17
+ This rule enforces the structural contract:
18
+
19
+ - `package.json` must contain an `n8n` object.
20
+ - `n8n.n8nNodesApiVersion` must be present and a positive integer. It must live
21
+ inside `n8n`, not at the root.
22
+ - `n8n.nodes` must be a non-empty array of strings, each starting with `dist/`.
23
+ - `n8n.credentials`, if present, must be an array of strings, each starting
24
+ with `dist/`.
25
+
26
+ The `dist/` prefix is required because community nodes ship compiled
27
+ JavaScript; n8n cannot load TypeScript sources at runtime. Variants such as
28
+ `./dist/...` or `DIST/...` are rejected to keep the convention consistent with
29
+ the templates published by `@n8n/node-cli` and `@n8n/create-node`.
30
+
31
+ ## Examples
32
+
33
+ ### Incorrect
34
+
35
+ ```json
36
+ {
37
+ "name": "n8n-nodes-example",
38
+ "version": "1.0.0"
39
+ }
40
+ ```
41
+
42
+ ```json
43
+ {
44
+ "name": "n8n-nodes-example",
45
+ "n8nNodesApiVersion": 1,
46
+ "n8n": {
47
+ "nodes": ["dist/nodes/Foo/Foo.node.js"]
48
+ }
49
+ }
50
+ ```
51
+
52
+ ```json
53
+ {
54
+ "name": "n8n-nodes-example",
55
+ "n8n": {
56
+ "n8nNodesApiVersion": "1",
57
+ "nodes": ["nodes/Foo/Foo.node.js"]
58
+ }
59
+ }
60
+ ```
61
+
62
+ ```json
63
+ {
64
+ "name": "n8n-nodes-example",
65
+ "n8n": {
66
+ "n8nNodesApiVersion": 1,
67
+ "nodes": []
68
+ }
69
+ }
70
+ ```
71
+
72
+ ```json
73
+ {
74
+ "name": "n8n-nodes-example",
75
+ "n8n": {
76
+ "n8nNodesApiVersion": 1,
77
+ "nodes": ["./dist/nodes/Foo/Foo.node.js"]
78
+ }
79
+ }
80
+ ```
81
+
82
+ ### Correct
83
+
84
+ ```json
85
+ {
86
+ "name": "n8n-nodes-example",
87
+ "n8n": {
88
+ "n8nNodesApiVersion": 1,
89
+ "nodes": ["dist/nodes/Foo/Foo.node.js"],
90
+ "credentials": ["dist/credentials/Foo.credentials.js"]
91
+ }
92
+ }
93
+ ```
@@ -6,13 +6,13 @@
6
6
 
7
7
  ## Rule Details
8
8
 
9
- The `overrides` field in `package.json` lets a package force specific versions of its (transitive) dependencies. In the context of n8n community nodes this is dangerous:
9
+ The `overrides` field in `package.json` forces specific versions of (transitive) dependencies. n8n installs each community package into an isolated `node_modules` tree (peer deps stripped before install, `require()` walks up from each node's compiled file), so an override in one node only affects that node's own resolution — it does **not** bleed into other nodes or into n8n core. The rule bans the field anyway because:
10
10
 
11
- - Community nodes are installed into a shared n8n runtime alongside other nodes. Overriding a shared library (e.g. `axios`, `@langchain/core`, `minimatch`) can silently substitute an incompatible version for every other node that depends on it, causing hard-to-diagnose runtime failures.
12
- - Community nodes are distributed as pre-built packages with their dependencies already bundled or declared as `peerDependencies`. Any version pinning that the node actually needs should happen during development, not at install time on the user's n8n instance.
13
- - `overrides` is frequently copy-pasted from an unrelated internal project and is almost never intentional in a community node.
11
+ - **Almost always unintentional.** In practice, `overrides` blocks in community nodes are copy-pasted boilerplate from unrelated projects, sometimes alongside an empty `dependencies` so the override is a literal no-op.
12
+ - **No useful effect today.** Because of isolation, a maintainer who believes their override coordinates versions across nodes is wrong about what it does. The block is dead weight at best, actively misleading at worst.
13
+ - **Future-proofing.** If the install layout ever moves toward hoisting or partial sharing, today's "harmless" overrides start affecting other nodes' resolution. Banning the field now keeps that change safe to make.
14
14
 
15
- If you have a genuine compatibility need, bundle the dependency into the published artifact or declare it via `peerDependencies` instead.
15
+ Most community nodes do not need third-party runtime libraries at all. n8n core already provides HTTP requests (`this.helpers.httpRequest`, `this.helpers.httpRequestWithAuthentication`), credential resolution, binary data helpers, and other common building blocks via the execute context — these should be the default. `dependencies` and `peerDependencies` are restricted by [`no-runtime-dependencies`](no-runtime-dependencies.md) and [`valid-peer-dependencies`](valid-peer-dependencies.md) respectively, so neither is a workaround for `overrides`.
16
16
 
17
17
  ## Examples
18
18
 
@@ -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
+ ```
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.15.0",
4
+ "version": "0.16.0",
5
5
  "main": "./dist/plugin.js",
6
6
  "types": "./dist/plugin.d.ts",
7
7
  "exports": {
@@ -24,7 +24,7 @@
24
24
  "vitest": "^4.1.1",
25
25
  "@n8n/typescript-config": "1.4.0",
26
26
  "@n8n/vitest-config": "1.10.0",
27
- "n8n-workflow": "2.20.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',
@@ -33,13 +34,18 @@ const configs = {
33
34
  '@n8n/community-nodes/no-http-request-with-manual-auth': 'error',
34
35
  '@n8n/community-nodes/no-overrides-field': 'error',
35
36
  '@n8n/community-nodes/no-runtime-dependencies': 'error',
37
+ '@n8n/community-nodes/no-template-placeholders': 'error',
36
38
  '@n8n/community-nodes/icon-validation': 'error',
37
39
  '@n8n/community-nodes/options-sorted-alphabetically': 'warn',
38
40
  '@n8n/community-nodes/resource-operation-pattern': 'warn',
39
41
  '@n8n/community-nodes/credential-documentation-url': 'error',
40
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',
41
45
  '@n8n/community-nodes/node-connection-type-literal': 'error',
42
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',
43
49
  '@n8n/community-nodes/require-community-node-keyword': 'warn',
44
50
  '@n8n/community-nodes/require-continue-on-fail': 'error',
45
51
  '@n8n/community-nodes/require-node-api-error': 'error',
@@ -57,6 +63,7 @@ const configs = {
57
63
  rules: {
58
64
  '@n8n/community-nodes/ai-node-package-json': 'error',
59
65
  '@n8n/community-nodes/credential-password-field': 'error',
66
+ '@n8n/community-nodes/n8n-object-validation': 'error',
60
67
  '@n8n/community-nodes/no-deprecated-workflow-functions': 'error',
61
68
  '@n8n/community-nodes/node-usable-as-tool': 'error',
62
69
  '@n8n/community-nodes/package-name-convention': 'error',
@@ -66,13 +73,18 @@ const configs = {
66
73
  '@n8n/community-nodes/no-http-request-with-manual-auth': 'error',
67
74
  '@n8n/community-nodes/no-overrides-field': 'error',
68
75
  '@n8n/community-nodes/no-runtime-dependencies': 'error',
76
+ '@n8n/community-nodes/no-template-placeholders': 'error',
69
77
  '@n8n/community-nodes/icon-validation': 'error',
70
78
  '@n8n/community-nodes/options-sorted-alphabetically': 'warn',
71
79
  '@n8n/community-nodes/credential-documentation-url': 'error',
72
80
  '@n8n/community-nodes/resource-operation-pattern': 'warn',
73
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',
74
84
  '@n8n/community-nodes/node-connection-type-literal': 'error',
75
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',
76
88
  '@n8n/community-nodes/require-community-node-keyword': 'warn',
77
89
  '@n8n/community-nodes/require-continue-on-fail': 'error',
78
90
  '@n8n/community-nodes/require-node-api-error': 'error',
@@ -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
+ });