From de20c564c791ffb8875da38bbf9ad24ca4f06c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20M?= Date: Fri, 19 Jul 2024 19:17:57 +0200 Subject: [PATCH] feat: add eslint rule for enforcing WorkspaceService naming convention (#6308) ### Description This PR introduces a custom ESLint rule named `inject-workspace-repository`. The purpose of this rule is to enforce naming conventions for files and classes that use the `@InjectWorkspaceRepository` decorator or include services ending with `WorkspaceService` in their constructors. ### Rule Overview The new ESLint rule checks for the following conditions: 1. **File Naming**: - Only file ending with `.service.ts` or `.workspace-service.ts` are checked. - If a file contains a class using the `@InjectWorkspaceRepository` decorator or a service ending with `WorkspaceService` in the constructor, the file name must end with `.workspace-service.ts`. 2. **Class Naming**: - Classes that use the `@InjectWorkspaceRepository` decorator or include services ending with `WorkspaceService` in their constructors must have names that end with `WorkspaceService`. ### How It Works The rule inspects each TypeScript file to ensure that the naming conventions are adhered to. It specifically looks for: - Constructor parameters with the `@InjectWorkspaceRepository` decorator. - Constructor parameters with a type annotation ending with `WorkspaceService`. When such parameters are found, it checks the class name and the file name to ensure they conform to the expected patterns. ### Example Code #### Valid Cases 1. **Correct File and Class Name with Decorator**: ```typescript // Filename: my.workspace-service.ts class MyWorkspaceService { constructor(@InjectWorkspaceRepository() private repository) {} } ``` 2. **Service Dependency**: ```typescript // Filename: another.workspace-service.ts class AnotherWorkspaceService { constructor(private myWorkspaceService: MyWorkspaceService) {} } ``` #### Invalid Cases 1. **Incorrect Class Name**: ```typescript // Filename: my.workspace-service.ts class MyService { constructor(@InjectWorkspaceRepository() private repository) {} } // Error: Class name should end with 'WorkspaceService'. ``` 2. **Incorrect File Name**: ```typescript // Filename: my.service.ts class MyWorkspaceService { constructor(@InjectWorkspaceRepository() private repository) {} } // Error: File name should end with '.workspace-service.ts'. ``` 3. **Incorrect File and Class Name**: ```typescript // Filename: my.service.ts class MyService { constructor(@InjectWorkspaceRepository() private repository) {} } // Error: Class name should end with 'WorkspaceService'. // Error: File name should end with '.workspace-service.ts'. ``` 4. **Incorrect File Type**: ```typescript // Filename: another.service.ts class AnotherService { constructor(private myWorkspaceService: MyWorkspaceService) {} } // Error: Class name should end with 'WorkspaceService'. // Error: File name should end with '.workspace-service.ts'. ``` 5. **Incorrect Class Name with Dependency**: ```typescript // Filename: another.workspace-service.ts class AnotherService { constructor(private myWorkspaceService: MyWorkspaceService) {} } // Error: Class name should end with 'WorkspaceService'. ``` ### First step This rule is only a warning for now, and then we'll migrate all the code that need to be migrated and move from `warn` to `error`. Fix #6309 Co-authored-by: Charles Bochet --- packages/twenty-server/.eslintrc.cjs | 1 + .../engine/twenty-orm/twenty-orm.providers.ts | 1 + tools/eslint-rules/index.ts | 5 ++ .../rules/inject-workspace-repository.spec.ts | 77 +++++++++++++++++ .../rules/inject-workspace-repository.ts | 86 +++++++++++++++++++ 5 files changed, 170 insertions(+) create mode 100644 tools/eslint-rules/rules/inject-workspace-repository.spec.ts create mode 100644 tools/eslint-rules/rules/inject-workspace-repository.ts diff --git a/packages/twenty-server/.eslintrc.cjs b/packages/twenty-server/.eslintrc.cjs index af85a600d9df..3486687757ce 100644 --- a/packages/twenty-server/.eslintrc.cjs +++ b/packages/twenty-server/.eslintrc.cjs @@ -90,6 +90,7 @@ module.exports = { 'unicorn/filename-case': 'off', 'prefer-arrow/prefer-arrow-functions': 'off', '@nx/workspace-max-consts-per-file': 'off', + '@nx/workspace-inject-workspace-repository': 'warn', }, }, ], diff --git a/packages/twenty-server/src/engine/twenty-orm/twenty-orm.providers.ts b/packages/twenty-server/src/engine/twenty-orm/twenty-orm.providers.ts index 4668364bf836..ca0afc7eea30 100644 --- a/packages/twenty-server/src/engine/twenty-orm/twenty-orm.providers.ts +++ b/packages/twenty-server/src/engine/twenty-orm/twenty-orm.providers.ts @@ -24,6 +24,7 @@ export function createTwentyORMProviders( ); if (!dataSource) { + // TODO: Throw here when the code is well architected return null; } diff --git a/tools/eslint-rules/index.ts b/tools/eslint-rules/index.ts index 3367634ce323..98419b674d32 100644 --- a/tools/eslint-rules/index.ts +++ b/tools/eslint-rules/index.ts @@ -1,3 +1,7 @@ +import { + RULE_NAME as injectWorkspaceRepositoryName, + rule as injectWorkspaceRepository, +} from './rules/inject-workspace-repository'; import { rule as componentPropsNaming, RULE_NAME as componentPropsNamingName, @@ -88,5 +92,6 @@ module.exports = { [useRecoilCallbackHasDependencyArrayName]: useRecoilCallbackHasDependencyArray, [noNavigatePreferLinkName]: noNavigatePreferLink, + [injectWorkspaceRepositoryName]: injectWorkspaceRepository, }, }; diff --git a/tools/eslint-rules/rules/inject-workspace-repository.spec.ts b/tools/eslint-rules/rules/inject-workspace-repository.spec.ts new file mode 100644 index 000000000000..27b136a87dc1 --- /dev/null +++ b/tools/eslint-rules/rules/inject-workspace-repository.spec.ts @@ -0,0 +1,77 @@ +import { TSESLint } from '@typescript-eslint/utils'; +import { rule, RULE_NAME } from './inject-workspace-repository'; + +const ruleTester = new TSESLint.RuleTester({ + parser: require.resolve('@typescript-eslint/parser'), +}); + +ruleTester.run(RULE_NAME, rule, { + valid: [ + { + code: ` + class MyWorkspaceService { + constructor(@InjectWorkspaceRepository() private repository) {} + } + `, + filename: 'my.workspace-service.ts', + }, + { + code: ` + class AnotherWorkspaceService { + constructor(private myWorkspaceService: MyWorkspaceService) {} + } + `, + filename: 'another.workspace-service.ts', + }, + ], + invalid: [ + { + code: ` + class MyService { + constructor(@InjectWorkspaceRepository() private repository) {} + } + `, + filename: 'my.workspace-service.ts', + errors: [{ messageId: 'invalidClassName' }], + }, + { + code: ` + class MyWorkspaceService { + constructor(@InjectWorkspaceRepository() private repository) {} + } + `, + filename: 'my.service.ts', + errors: [{ messageId: 'invalidFileName' }], + }, + { + code: ` + class MyService { + constructor(@InjectWorkspaceRepository() private repository) {} + } + `, + filename: 'my.service.ts', + errors: [ + { messageId: 'invalidClassName' }, + { messageId: 'invalidFileName' }, + ], + }, + { + code: ` + class AnotherWorkspaceService { + constructor(private myWorkspaceService: MyWorkspaceService) {} + } + `, + filename: 'another.service.ts', + errors: [{ messageId: 'invalidFileName' }], + }, + { + code: ` + class AnotherService { + constructor(private myWorkspaceService: MyWorkspaceService) {} + } + `, + filename: 'another.workspace-service.ts', + errors: [{ messageId: 'invalidClassName' }], + }, + ], +}); diff --git a/tools/eslint-rules/rules/inject-workspace-repository.ts b/tools/eslint-rules/rules/inject-workspace-repository.ts new file mode 100644 index 000000000000..be45d98953d2 --- /dev/null +++ b/tools/eslint-rules/rules/inject-workspace-repository.ts @@ -0,0 +1,86 @@ +import { ESLintUtils, TSESTree } from '@typescript-eslint/utils'; + +export const RULE_NAME = 'inject-workspace-repository'; + +export const rule = ESLintUtils.RuleCreator(() => __filename)({ + name: RULE_NAME, + meta: { + type: 'problem', + docs: { + description: + 'Ensure class names and file names follow the required pattern when using @InjectWorkspaceRepository.', + recommended: 'recommended', + }, + schema: [], + messages: { + invalidClassName: "Class name should end with 'WorkspaceService'.", + invalidFileName: "File name should end with '.workspace-service.ts'.", + }, + }, + defaultOptions: [], + create: (context) => { + return { + MethodDefinition: (node: TSESTree.MethodDefinition) => { + const filename = context.filename; + + // Only check files that end with '.workspace-service.ts' or '.service.ts' + if ( + !filename.endsWith('.workspace-service.ts') && + !filename.endsWith('.service.ts') + ) { + return; + } + + if (node.kind === 'constructor') { + const hasInjectWorkspaceRepositoryDecoratorOrWorkspaceService = + node.value.params.some((param) => { + if (param.type === TSESTree.AST_NODE_TYPES.TSParameterProperty) { + const hasDecorator = param.decorators?.some((decorator) => { + return ( + decorator.expression.type === + TSESTree.AST_NODE_TYPES.CallExpression && + (decorator.expression.callee as TSESTree.Identifier) + .name === 'InjectWorkspaceRepository' + ); + }); + const hasWorkspaceServiceType = + param.parameter.typeAnnotation?.typeAnnotation && + param.parameter.typeAnnotation.typeAnnotation.type === + TSESTree.AST_NODE_TYPES.TSTypeReference && + ( + param.parameter.typeAnnotation.typeAnnotation + .typeName as TSESTree.Identifier + ).name.endsWith('WorkspaceService'); + + return hasDecorator || hasWorkspaceServiceType; + } + + return false; + }); + + if (hasInjectWorkspaceRepositoryDecoratorOrWorkspaceService) { + const className = (node.parent.parent as TSESTree.ClassDeclaration) + .id?.name; + const filename = context.filename; + + if (!className?.endsWith('WorkspaceService')) { + context.report({ + node: node.parent, + messageId: 'invalidClassName', + }); + } + + if (!filename.endsWith('.workspace-service.ts')) { + context.report({ + node: node.parent, + messageId: 'invalidFileName', + }); + } + } + } + }, + }; + }, +}); + +export default rule;