mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-12 16:10:30 +02:00
feat: Add cred-class-name-suffix lint rule (no-changelog) (#29801)
This commit is contained in:
parent
64079ad98c
commit
31f577a39f
|
|
@ -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 | ✅ ☑️ | | 🔧 | | |
|
||||
|
|
|
|||
|
|
@ -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[] = [];
|
||||
}
|
||||
```
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user