mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-12 16:10:30 +02:00
Merge remote-tracking branch 'origin/master' into ce-877-eslint-cred-class-name-suffix-credential-class-names-must
Some checks failed
CI: Python / Checks (push) Has been cancelled
Some checks failed
CI: Python / Checks (push) Has been cancelled
This commit is contained in:
commit
a391839fc6
|
|
@ -49,6 +49,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 | ✅ ☑️ | | 🔧 | | |
|
||||
| [credential-test-required](docs/rules/credential-test-required.md) | Ensure credentials have a credential test | ✅ ☑️ | | | 💡 | |
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
# OAuth2 credentials must include `OAuth2` in the class name, `name`, and `displayName` (`@n8n/community-nodes/cred-class-oauth2-naming`)
|
||||
|
||||
💼 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
|
||||
|
||||
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.
|
||||
|
||||
A credential class is considered an OAuth2 credential if **any** of the following are true:
|
||||
|
||||
- The class name contains `OAuth2` (e.g. `GoogleOAuth2Api`).
|
||||
- The class TypeScript-extends a base whose name contains `OAuth2` (e.g. `class GoogleOAuth2Api extends OAuth2Api`).
|
||||
- The `extends` class field references an `OAuth2` credential (e.g. `extends = ['oAuth2Api']`).
|
||||
- The `name` or `displayName` field contains `OAuth2`.
|
||||
|
||||
When a credential is detected as OAuth2, all of the following must contain `OAuth2`:
|
||||
|
||||
- Class name (must end with `OAuth2Api`).
|
||||
- `name` class field.
|
||||
- `displayName` class field.
|
||||
|
||||
The class name is auto-fixable; `name` and `displayName` must be updated manually because their casing convention varies.
|
||||
|
||||
## Examples
|
||||
|
||||
### ❌ Incorrect
|
||||
|
||||
```typescript
|
||||
// Class name detected as OAuth2 but `name` and `displayName` lack OAuth2.
|
||||
export class GoogleOAuth2Api implements ICredentialType {
|
||||
name = 'googleApi';
|
||||
displayName = 'Google API';
|
||||
properties: INodeProperties[] = [];
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// Extends OAuth2 base, but neither class name, `name`, nor `displayName` reflect that.
|
||||
export class GoogleApi implements ICredentialType {
|
||||
name = 'googleApi';
|
||||
displayName = 'Google API';
|
||||
extends = ['oAuth2Api'];
|
||||
properties: INodeProperties[] = [];
|
||||
}
|
||||
```
|
||||
|
||||
### ✅ Correct
|
||||
|
||||
```typescript
|
||||
export class GoogleOAuth2Api implements ICredentialType {
|
||||
name = 'googleOAuth2Api';
|
||||
displayName = 'Google OAuth2 API';
|
||||
extends = ['oAuth2Api'];
|
||||
properties: INodeProperties[] = [];
|
||||
}
|
||||
```
|
||||
|
||||
## Migrated from
|
||||
|
||||
This rule consolidates three rules from the legacy `eslint-plugin-n8n-nodes-base` plugin into a single conceptual check:
|
||||
|
||||
- `cred-class-name-missing-oauth2-suffix`
|
||||
- `cred-class-field-name-missing-oauth2`
|
||||
- `cred-class-field-display-name-missing-oauth2`
|
||||
|
|
@ -41,6 +41,7 @@ const configs = {
|
|||
'@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',
|
||||
'@n8n/community-nodes/node-operation-error-itemindex': 'error',
|
||||
|
|
@ -77,6 +78,8 @@ 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',
|
||||
'@n8n/community-nodes/node-operation-error-itemindex': 'error',
|
||||
|
|
@ -86,7 +89,6 @@ const configs = {
|
|||
'@n8n/community-nodes/require-node-description-fields': 'error',
|
||||
'@n8n/community-nodes/valid-credential-references': 'error',
|
||||
'@n8n/community-nodes/valid-peer-dependencies': 'error',
|
||||
'@n8n/community-nodes/cred-class-name-suffix': 'error',
|
||||
'@n8n/community-nodes/webhook-lifecycle-complete': 'error',
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,197 @@
|
|||
import { RuleTester } from '@typescript-eslint/rule-tester';
|
||||
|
||||
import { CredClassOAuth2NamingRule } from './cred-class-oauth2-naming.js';
|
||||
|
||||
const ruleTester = new RuleTester();
|
||||
|
||||
const credFilePath = '/tmp/TestCredential.credentials.ts';
|
||||
const nonCredFilePath = '/tmp/SomeHelper.ts';
|
||||
|
||||
type CredentialFields = {
|
||||
className: string;
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
extendsValues?: string[];
|
||||
superClass?: string;
|
||||
};
|
||||
|
||||
function createCredentialCode(fields: CredentialFields): string {
|
||||
const { className, name, displayName, extendsValues, superClass } = fields;
|
||||
|
||||
const heritage = superClass ? ` extends ${superClass}` : '';
|
||||
const lines: string[] = [];
|
||||
if (name !== undefined) lines.push(`\tname = '${name}';`);
|
||||
if (displayName !== undefined) lines.push(`\tdisplayName = '${displayName}';`);
|
||||
if (extendsValues !== undefined) {
|
||||
const arr = extendsValues.map((v) => `'${v}'`).join(', ');
|
||||
lines.push(`\textends = [${arr}];`);
|
||||
}
|
||||
lines.push('\tproperties: INodeProperties[] = [];');
|
||||
|
||||
return `
|
||||
import type { ICredentialType, INodeProperties } from 'n8n-workflow';
|
||||
|
||||
export class ${className}${heritage} implements ICredentialType {
|
||||
${lines.join('\n')}
|
||||
}`;
|
||||
}
|
||||
|
||||
function createRegularClass(): string {
|
||||
return `
|
||||
export class SomeHelper {
|
||||
name = 'helper';
|
||||
}`;
|
||||
}
|
||||
|
||||
ruleTester.run('cred-class-oauth2-naming', CredClassOAuth2NamingRule, {
|
||||
valid: [
|
||||
{
|
||||
name: 'non-OAuth2 credential is ignored',
|
||||
filename: credFilePath,
|
||||
code: createCredentialCode({
|
||||
className: 'GoogleApi',
|
||||
name: 'googleApi',
|
||||
displayName: 'Google API',
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'OAuth2 credential with all naming correct',
|
||||
filename: credFilePath,
|
||||
code: createCredentialCode({
|
||||
className: 'GoogleOAuth2Api',
|
||||
name: 'googleOAuth2Api',
|
||||
displayName: 'Google OAuth2 API',
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'OAuth2 credential detected via extends array, all naming correct',
|
||||
filename: credFilePath,
|
||||
code: createCredentialCode({
|
||||
className: 'GoogleOAuth2Api',
|
||||
name: 'googleOAuth2Api',
|
||||
displayName: 'Google OAuth2 API',
|
||||
extendsValues: ['oAuth2Api'],
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'OAuth2 credential detected via TS superClass, all naming correct',
|
||||
filename: credFilePath,
|
||||
code: createCredentialCode({
|
||||
className: 'GoogleOAuth2Api',
|
||||
name: 'googleOAuth2Api',
|
||||
displayName: 'Google OAuth2 API',
|
||||
superClass: 'OAuth2Api',
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'class not implementing ICredentialType is ignored',
|
||||
filename: credFilePath,
|
||||
code: createRegularClass(),
|
||||
},
|
||||
{
|
||||
name: 'non-.credentials.ts file is ignored',
|
||||
filename: nonCredFilePath,
|
||||
code: createCredentialCode({
|
||||
className: 'GoogleApi',
|
||||
name: 'googleOAuth2Api',
|
||||
displayName: 'Google OAuth2 API',
|
||||
}),
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
name: 'class name missing OAuth2 (detected via name field), with Api suffix',
|
||||
filename: credFilePath,
|
||||
code: createCredentialCode({
|
||||
className: 'GoogleApi',
|
||||
name: 'googleOAuth2Api',
|
||||
displayName: 'Google OAuth2 API',
|
||||
}),
|
||||
errors: [{ messageId: 'classNameMissingOAuth2', data: { name: 'GoogleApi' } }],
|
||||
output: createCredentialCode({
|
||||
className: 'GoogleOAuth2Api',
|
||||
name: 'googleOAuth2Api',
|
||||
displayName: 'Google OAuth2 API',
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'class name missing OAuth2 (detected via displayName), no Api suffix',
|
||||
filename: credFilePath,
|
||||
code: createCredentialCode({
|
||||
className: 'Google',
|
||||
name: 'googleOAuth2Api',
|
||||
displayName: 'Google OAuth2 API',
|
||||
}),
|
||||
errors: [{ messageId: 'classNameMissingOAuth2', data: { name: 'Google' } }],
|
||||
output: createCredentialCode({
|
||||
className: 'GoogleOAuth2Api',
|
||||
name: 'googleOAuth2Api',
|
||||
displayName: 'Google OAuth2 API',
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'name field missing OAuth2 (detected via class name)',
|
||||
filename: credFilePath,
|
||||
code: createCredentialCode({
|
||||
className: 'GoogleOAuth2Api',
|
||||
name: 'googleApi',
|
||||
displayName: 'Google OAuth2 API',
|
||||
}),
|
||||
errors: [{ messageId: 'nameMissingOAuth2', data: { value: 'googleApi' } }],
|
||||
output: null,
|
||||
},
|
||||
{
|
||||
name: 'displayName missing OAuth2 (detected via class name)',
|
||||
filename: credFilePath,
|
||||
code: createCredentialCode({
|
||||
className: 'GoogleOAuth2Api',
|
||||
name: 'googleOAuth2Api',
|
||||
displayName: 'Google API',
|
||||
}),
|
||||
errors: [{ messageId: 'displayNameMissingOAuth2', data: { value: 'Google API' } }],
|
||||
output: null,
|
||||
},
|
||||
{
|
||||
name: 'all three missing OAuth2, detected via extends array',
|
||||
filename: credFilePath,
|
||||
code: createCredentialCode({
|
||||
className: 'GoogleApi',
|
||||
name: 'googleApi',
|
||||
displayName: 'Google API',
|
||||
extendsValues: ['oAuth2Api'],
|
||||
}),
|
||||
errors: [
|
||||
{ messageId: 'classNameMissingOAuth2', data: { name: 'GoogleApi' } },
|
||||
{ messageId: 'nameMissingOAuth2', data: { value: 'googleApi' } },
|
||||
{ messageId: 'displayNameMissingOAuth2', data: { value: 'Google API' } },
|
||||
],
|
||||
output: createCredentialCode({
|
||||
className: 'GoogleOAuth2Api',
|
||||
name: 'googleApi',
|
||||
displayName: 'Google API',
|
||||
extendsValues: ['oAuth2Api'],
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'all three missing OAuth2, detected via TS superClass extends',
|
||||
filename: credFilePath,
|
||||
code: createCredentialCode({
|
||||
className: 'GoogleApi',
|
||||
name: 'googleApi',
|
||||
displayName: 'Google API',
|
||||
superClass: 'OAuth2Api',
|
||||
}),
|
||||
errors: [
|
||||
{ messageId: 'classNameMissingOAuth2', data: { name: 'GoogleApi' } },
|
||||
{ messageId: 'nameMissingOAuth2', data: { value: 'googleApi' } },
|
||||
{ messageId: 'displayNameMissingOAuth2', data: { value: 'Google API' } },
|
||||
],
|
||||
output: createCredentialCode({
|
||||
className: 'GoogleOAuth2Api',
|
||||
name: 'googleApi',
|
||||
displayName: 'Google API',
|
||||
superClass: 'OAuth2Api',
|
||||
}),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
import type { TSESTree } from '@typescript-eslint/utils';
|
||||
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
||||
|
||||
import {
|
||||
createRule,
|
||||
findClassProperty,
|
||||
getStringLiteralValue,
|
||||
isCredentialTypeClass,
|
||||
isFileType,
|
||||
} from '../utils/index.js';
|
||||
|
||||
const OAUTH2_PATTERN = /oauth2/i;
|
||||
|
||||
function containsOAuth2(value: string): boolean {
|
||||
return OAUTH2_PATTERN.test(value);
|
||||
}
|
||||
|
||||
function getExtendsArrayValues(node: TSESTree.PropertyDefinition): string[] {
|
||||
if (node.value?.type !== AST_NODE_TYPES.ArrayExpression) return [];
|
||||
const values: string[] = [];
|
||||
for (const element of node.value.elements) {
|
||||
const value = element ? getStringLiteralValue(element) : null;
|
||||
if (value !== null) values.push(value);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
function suggestOAuth2ApiName(className: string): string {
|
||||
if (className.endsWith('Api')) {
|
||||
return `${className.slice(0, -'Api'.length)}OAuth2Api`;
|
||||
}
|
||||
return `${className}OAuth2Api`;
|
||||
}
|
||||
|
||||
export const CredClassOAuth2NamingRule = createRule({
|
||||
name: 'cred-class-oauth2-naming',
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description:
|
||||
'OAuth2 credentials must include `OAuth2` in the class name, `name`, and `displayName`',
|
||||
},
|
||||
messages: {
|
||||
classNameMissingOAuth2: "OAuth2 credential class name '{{name}}' must end with 'OAuth2Api'",
|
||||
nameMissingOAuth2: "OAuth2 credential `name` field '{{value}}' must contain 'OAuth2'",
|
||||
displayNameMissingOAuth2:
|
||||
"OAuth2 credential `displayName` field '{{value}}' must contain 'OAuth2'",
|
||||
},
|
||||
schema: [],
|
||||
fixable: 'code',
|
||||
},
|
||||
defaultOptions: [],
|
||||
create(context) {
|
||||
if (!isFileType(context.filename, '.credentials.ts')) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
ClassDeclaration(node) {
|
||||
if (!isCredentialTypeClass(node)) return;
|
||||
if (!node.id) return;
|
||||
|
||||
const className = node.id.name;
|
||||
const superClassName =
|
||||
node.superClass?.type === AST_NODE_TYPES.Identifier ? node.superClass.name : null;
|
||||
|
||||
const nameProperty = findClassProperty(node, 'name');
|
||||
const nameValue = nameProperty?.value ? getStringLiteralValue(nameProperty.value) : null;
|
||||
|
||||
const displayNameProperty = findClassProperty(node, 'displayName');
|
||||
const displayNameValue = displayNameProperty?.value
|
||||
? getStringLiteralValue(displayNameProperty.value)
|
||||
: null;
|
||||
|
||||
const extendsProperty = findClassProperty(node, 'extends');
|
||||
const extendsValues = extendsProperty ? getExtendsArrayValues(extendsProperty) : [];
|
||||
|
||||
const isOAuth2Credential =
|
||||
containsOAuth2(className) ||
|
||||
(superClassName !== null && containsOAuth2(superClassName)) ||
|
||||
extendsValues.some(containsOAuth2) ||
|
||||
(nameValue !== null && containsOAuth2(nameValue)) ||
|
||||
(displayNameValue !== null && containsOAuth2(displayNameValue));
|
||||
|
||||
if (!isOAuth2Credential) return;
|
||||
|
||||
if (!className.endsWith('OAuth2Api')) {
|
||||
const fixedClassName = suggestOAuth2ApiName(className);
|
||||
|
||||
context.report({
|
||||
node: node.id,
|
||||
messageId: 'classNameMissingOAuth2',
|
||||
data: { name: className },
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(node.id!, fixedClassName);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (nameValue !== null && !containsOAuth2(nameValue)) {
|
||||
context.report({
|
||||
node: nameProperty!.value!,
|
||||
messageId: 'nameMissingOAuth2',
|
||||
data: { value: nameValue },
|
||||
});
|
||||
}
|
||||
|
||||
if (displayNameValue !== null && !containsOAuth2(displayNameValue)) {
|
||||
context.report({
|
||||
node: displayNameProperty!.value!,
|
||||
messageId: 'displayNameMissingOAuth2',
|
||||
data: { value: displayNameValue },
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
@ -3,6 +3,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';
|
||||
import { CredentialTestRequiredRule } from './credential-test-required.js';
|
||||
|
|
@ -55,6 +56,7 @@ export const rules = {
|
|||
'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,
|
||||
'missing-paired-item': MissingPairedItemRule,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user