feat: Add cred-class-name-suffix lint rule (no-changelog) (#29801)

This commit is contained in:
Garrit Franke 2026-05-06 18:00:49 +02:00 committed by GitHub
parent 64079ad98c
commit 31f577a39f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 182 additions and 0 deletions

View File

@ -48,6 +48,7 @@ export default [
| :--------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------ | :--- | :--- | :- | :- | :- |
| [ai-node-package-json](docs/rules/ai-node-package-json.md) | Enforce consistency between n8n.aiNodeSdkVersion and ai-node-sdk peer dependency in community node packages | ✅ ☑️ | | | | |
| [cred-class-field-icon-missing](docs/rules/cred-class-field-icon-missing.md) | Credential class must have an `icon` property defined | ✅ ☑️ | | | 💡 | |
| [cred-class-name-suffix](docs/rules/cred-class-name-suffix.md) | Credential class names must be suffixed with `Api` | ✅ ☑️ | | 🔧 | | |
| [cred-class-oauth2-naming](docs/rules/cred-class-oauth2-naming.md) | OAuth2 credentials must include `OAuth2` in the class name, `name`, and `displayName` | ✅ ☑️ | | 🔧 | | |
| [credential-documentation-url](docs/rules/credential-documentation-url.md) | Enforce valid credential documentationUrl format (URL or lowercase alphanumeric slug) | ✅ ☑️ | | 🔧 | | |
| [credential-password-field](docs/rules/credential-password-field.md) | Ensure credential fields with sensitive names have typeOptions.password = true | ✅ ☑️ | | 🔧 | | |

View File

@ -0,0 +1,46 @@
# Credential class names must be suffixed with `Api` (`@n8n/community-nodes/cred-class-name-suffix`)
💼 This rule is enabled in the following configs: ✅ `recommended`, ☑️ `recommendedWithoutN8nCloudSupport`.
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
<!-- end auto-generated rule header -->
## Rule Details
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.
For OAuth2 credentials extending the OAuth2 base, see the sibling rule `cred-class-oauth2-naming` which enforces the more specific `OAuth2Api` suffix.
## Opt-out
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:
```typescript
// eslint-disable-next-line @n8n/community-nodes/cred-class-name-suffix
export class CustomAuthHeader implements ICredentialType {
// ...
}
```
## Examples
### ❌ Incorrect
```typescript
export class MyService implements ICredentialType {
name = 'myServiceApi';
displayName = 'My Service API';
properties: INodeProperties[] = [];
}
```
### ✅ Correct
```typescript
export class MyServiceApi implements ICredentialType {
name = 'myServiceApi';
displayName = 'My Service API';
properties: INodeProperties[] = [];
}
```

View File

@ -40,6 +40,7 @@ const configs = {
'@n8n/community-nodes/resource-operation-pattern': 'warn',
'@n8n/community-nodes/credential-documentation-url': 'error',
'@n8n/community-nodes/cred-class-field-icon-missing': 'error',
'@n8n/community-nodes/cred-class-name-suffix': 'error',
'@n8n/community-nodes/cred-class-oauth2-naming': 'error',
'@n8n/community-nodes/node-connection-type-literal': 'error',
'@n8n/community-nodes/missing-paired-item': 'error',
@ -77,6 +78,7 @@ const configs = {
'@n8n/community-nodes/credential-documentation-url': 'error',
'@n8n/community-nodes/resource-operation-pattern': 'warn',
'@n8n/community-nodes/cred-class-field-icon-missing': 'error',
'@n8n/community-nodes/cred-class-name-suffix': 'error',
'@n8n/community-nodes/cred-class-oauth2-naming': 'error',
'@n8n/community-nodes/node-connection-type-literal': 'error',
'@n8n/community-nodes/missing-paired-item': 'error',

View File

@ -0,0 +1,74 @@
import { RuleTester } from '@typescript-eslint/rule-tester';
import { CredClassNameSuffixRule } from './cred-class-name-suffix.js';
const ruleTester = new RuleTester();
const credFilePath = '/tmp/TestCredential.credentials.ts';
const nonCredFilePath = '/tmp/SomeHelper.ts';
function createCredentialCode(className: string): string {
return `
import type { ICredentialType, INodeProperties } from 'n8n-workflow';
export class ${className} implements ICredentialType {
name = 'testApi';
displayName = 'Test API';
properties: INodeProperties[] = [];
}`;
}
function createRegularClass(className: string): string {
return `
export class ${className} {
name = 'test';
}`;
}
ruleTester.run('cred-class-name-suffix', CredClassNameSuffixRule, {
valid: [
{
name: 'credential class with Api suffix',
filename: credFilePath,
code: createCredentialCode('TestApi'),
},
{
name: 'credential class with OAuth2Api suffix',
filename: credFilePath,
code: createCredentialCode('TestOAuth2Api'),
},
{
name: 'class not implementing ICredentialType is ignored',
filename: credFilePath,
code: createRegularClass('SomeHelper'),
},
{
name: 'non-.credentials.ts file is ignored',
filename: nonCredFilePath,
code: createCredentialCode('TestCredential'),
},
],
invalid: [
{
name: 'credential class missing Api suffix',
filename: credFilePath,
code: createCredentialCode('TestCredential'),
errors: [{ messageId: 'missingSuffix', data: { name: 'TestCredential' } }],
output: createCredentialCode('TestCredentialApi'),
},
{
name: 'credential class name ending in Ap',
filename: credFilePath,
code: createCredentialCode('TestAp'),
errors: [{ messageId: 'missingSuffix', data: { name: 'TestAp' } }],
output: createCredentialCode('TestApi'),
},
{
name: 'credential class name ending in A',
filename: credFilePath,
code: createCredentialCode('TestA'),
errors: [{ messageId: 'missingSuffix', data: { name: 'TestA' } }],
output: createCredentialCode('TestApi'),
},
],
});

View File

@ -0,0 +1,57 @@
import { isCredentialTypeClass, isFileType, createRule } from '../utils/index.js';
function addApiSuffix(name: string): string {
if (name.endsWith('Ap')) return `${name}i`;
if (name.endsWith('A')) return `${name}pi`;
return `${name}Api`;
}
export const CredClassNameSuffixRule = createRule({
name: 'cred-class-name-suffix',
meta: {
type: 'problem',
docs: {
description: 'Credential class names must be suffixed with `Api`',
},
messages: {
missingSuffix: "Credential class name '{{name}}' must end with 'Api'",
},
fixable: 'code',
schema: [],
},
defaultOptions: [],
create(context) {
if (!isFileType(context.filename, '.credentials.ts')) {
return {};
}
return {
ClassDeclaration(node) {
if (!isCredentialTypeClass(node)) {
return;
}
const classNameNode = node.id;
if (!classNameNode) {
return;
}
const className = classNameNode.name;
if (className.endsWith('Api')) {
return;
}
const fixedName = addApiSuffix(className);
context.report({
node: classNameNode,
messageId: 'missingSuffix',
data: { name: className },
fix(fixer) {
return fixer.replaceText(classNameNode, fixedName);
},
});
},
};
},
});

View File

@ -2,6 +2,7 @@ import type { AnyRuleModule } from '@typescript-eslint/utils/ts-eslint';
import { AiNodePackageJsonRule } from './ai-node-package-json.js';
import { CredClassFieldIconMissingRule } from './cred-class-field-icon-missing.js';
import { CredClassNameSuffixRule } from './cred-class-name-suffix.js';
import { CredClassOAuth2NamingRule } from './cred-class-oauth2-naming.js';
import { CredentialDocumentationUrlRule } from './credential-documentation-url.js';
import { CredentialPasswordFieldRule } from './credential-password-field.js';
@ -54,6 +55,7 @@ export const rules = {
'credential-documentation-url': CredentialDocumentationUrlRule,
'node-class-description-icon-missing': NodeClassDescriptionIconMissingRule,
'cred-class-field-icon-missing': CredClassFieldIconMissingRule,
'cred-class-name-suffix': CredClassNameSuffixRule,
'cred-class-oauth2-naming': CredClassOAuth2NamingRule,
'node-connection-type-literal': NodeConnectionTypeLiteralRule,
'node-operation-error-itemindex': NodeOperationErrorItemIndexRule,