mirror of
				https://github.com/lingble/twenty.git
				synced 2025-10-30 20:27:55 +00:00 
			
		
		
		
	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 <charles@twenty.com>
			
			
This commit is contained in:
		| @@ -90,6 +90,7 @@ module.exports = { | |||||||
|         'unicorn/filename-case': 'off', |         'unicorn/filename-case': 'off', | ||||||
|         'prefer-arrow/prefer-arrow-functions': 'off', |         'prefer-arrow/prefer-arrow-functions': 'off', | ||||||
|         '@nx/workspace-max-consts-per-file': 'off', |         '@nx/workspace-max-consts-per-file': 'off', | ||||||
|  |         '@nx/workspace-inject-workspace-repository': 'warn', | ||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|   ], |   ], | ||||||
|   | |||||||
| @@ -24,6 +24,7 @@ export function createTwentyORMProviders( | |||||||
|       ); |       ); | ||||||
|  |  | ||||||
|       if (!dataSource) { |       if (!dataSource) { | ||||||
|  |         // TODO: Throw here when the code is well architected | ||||||
|         return null; |         return null; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,3 +1,7 @@ | |||||||
|  | import { | ||||||
|  |   RULE_NAME as injectWorkspaceRepositoryName, | ||||||
|  |   rule as injectWorkspaceRepository, | ||||||
|  | } from './rules/inject-workspace-repository'; | ||||||
| import { | import { | ||||||
|   rule as componentPropsNaming, |   rule as componentPropsNaming, | ||||||
|   RULE_NAME as componentPropsNamingName, |   RULE_NAME as componentPropsNamingName, | ||||||
| @@ -88,5 +92,6 @@ module.exports = { | |||||||
|     [useRecoilCallbackHasDependencyArrayName]: |     [useRecoilCallbackHasDependencyArrayName]: | ||||||
|       useRecoilCallbackHasDependencyArray, |       useRecoilCallbackHasDependencyArray, | ||||||
|     [noNavigatePreferLinkName]: noNavigatePreferLink, |     [noNavigatePreferLinkName]: noNavigatePreferLink, | ||||||
|  |     [injectWorkspaceRepositoryName]: injectWorkspaceRepository, | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
|   | |||||||
							
								
								
									
										77
									
								
								tools/eslint-rules/rules/inject-workspace-repository.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								tools/eslint-rules/rules/inject-workspace-repository.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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' }], | ||||||
|  |     }, | ||||||
|  |   ], | ||||||
|  | }); | ||||||
							
								
								
									
										86
									
								
								tools/eslint-rules/rules/inject-workspace-repository.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								tools/eslint-rules/rules/inject-workspace-repository.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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; | ||||||
		Reference in New Issue
	
	Block a user
	 Jérémy M
					Jérémy M