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;