From 813c57fba6e055e79a571dd5f48ae17e86f1f2a3 Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Fri, 8 Nov 2024 12:40:11 +0100 Subject: [PATCH] Create workflow setup command (#8406) Steps to enable workflows: - enable feature flags - run metadata sync - run this command If the feature flag is not true, the command will fail. Which will be useful to prevent seeding a wrong workspace. Including a little fix for send email aciton error --- .../workflow-actions/code.workflow-action.ts | 16 +- .../commands/seed-workflow-views.command.ts | 185 ++++++++++++++++++ .../commands/workflow-command.module.ts | 20 ++ .../workflow/common/workflow-common.module.ts | 3 +- 4 files changed, 215 insertions(+), 9 deletions(-) create mode 100644 packages/twenty-server/src/modules/workflow/common/commands/seed-workflow-views.command.ts create mode 100644 packages/twenty-server/src/modules/workflow/common/commands/workflow-command.module.ts diff --git a/packages/twenty-server/src/modules/serverless/workflow-actions/code.workflow-action.ts b/packages/twenty-server/src/modules/serverless/workflow-actions/code.workflow-action.ts index 86adf74f89c4..39a66a87b289 100644 --- a/packages/twenty-server/src/modules/serverless/workflow-actions/code.workflow-action.ts +++ b/packages/twenty-server/src/modules/serverless/workflow-actions/code.workflow-action.ts @@ -21,16 +21,16 @@ export class CodeWorkflowAction implements WorkflowAction { async execute( workflowStepInput: WorkflowCodeStepInput, ): Promise { - const { workspaceId } = this.scopedWorkspaceContextFactory.create(); + try { + const { workspaceId } = this.scopedWorkspaceContextFactory.create(); - if (!workspaceId) { - throw new WorkflowStepExecutorException( - 'Scoped workspace not found', - WorkflowStepExecutorExceptionCode.SCOPED_WORKSPACE_NOT_FOUND, - ); - } + if (!workspaceId) { + throw new WorkflowStepExecutorException( + 'Scoped workspace not found', + WorkflowStepExecutorExceptionCode.SCOPED_WORKSPACE_NOT_FOUND, + ); + } - try { const result = await this.serverlessFunctionService.executeOneServerlessFunction( workflowStepInput.serverlessFunctionId, diff --git a/packages/twenty-server/src/modules/workflow/common/commands/seed-workflow-views.command.ts b/packages/twenty-server/src/modules/workflow/common/commands/seed-workflow-views.command.ts new file mode 100644 index 000000000000..62e969630b25 --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/common/commands/seed-workflow-views.command.ts @@ -0,0 +1,185 @@ +import { Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Command } from 'nest-commander'; +import { Repository } from 'typeorm'; +import { v4 } from 'uuid'; + +import { + ActiveWorkspacesCommandOptions, + ActiveWorkspacesCommandRunner, +} from 'src/database/commands/active-workspaces.command'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; + +@Command({ + name: 'workflow:seed:views', + description: 'Seed workflow views for workspace.', +}) +export class SeedWorkflowViewsCommand extends ActiveWorkspacesCommandRunner { + protected readonly logger: Logger; + + constructor( + @InjectRepository(Workspace, 'core') + protected readonly workspaceRepository: Repository, + @InjectRepository(ObjectMetadataEntity, 'metadata') + private readonly objectMetadataRepository: Repository, + + @InjectRepository(FieldMetadataEntity, 'metadata') + private readonly fieldMetadataRepository: Repository, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + ) { + super(workspaceRepository); + this.logger = new Logger(this.constructor.name); + } + + async executeActiveWorkspacesCommand( + _passedParam: string[], + _options: ActiveWorkspacesCommandOptions, + _workspaceIds: string[], + ): Promise { + const { dryRun } = _options; + + for (const workspaceId of _workspaceIds) { + await this.execute(workspaceId, dryRun); + } + } + + private async execute(workspaceId: string, dryRun = false): Promise { + this.logger.log(`Seeding workflow views for workspace: ${workspaceId}`); + + const workflowViewId = await this.seedView( + workspaceId, + 'workflow', + 'All Workflows', + ); + + await this.seedView( + workspaceId, + 'workflowVersion', + 'All Workflow Versions', + ); + + await this.seedView(workspaceId, 'workflowRun', 'All Workflow Runs'); + + const favoriteRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + 'favorite', + ); + + const existingFavorites = await favoriteRepository.find({ + where: { + viewId: workflowViewId, + }, + }); + + if (existingFavorites.length > 0) { + this.logger.log( + `Favorite already exists for view: ${existingFavorites[0].id}`, + ); + + return; + } + + if (dryRun) { + this.logger.log(`Dry run: Creating favorite for view: ${workflowViewId}`); + + return; + } + + await favoriteRepository.insert({ + viewId: workflowViewId, + position: 5, + }); + } + + private async seedView( + workspaceId: string, + nameSingular: string, + viewName: string, + dryRun = false, + ): Promise { + const objectMetadata = ( + await this.objectMetadataRepository.find({ + where: { workspaceId, nameSingular }, + }) + )?.[0]; + + if (!objectMetadata) { + throw new Error(`Object metadata not found: ${nameSingular}`); + } + + const fieldMetadataName = ( + await this.fieldMetadataRepository.find({ + where: { + workspaceId, + objectMetadataId: objectMetadata.id, + name: 'name', + }, + }) + )?.[0]; + + if (!fieldMetadataName) { + throw new Error( + `Field metadata not found for ${objectMetadata.id}: name`, + ); + } + + const viewRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + 'view', + ); + + const viewFieldRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + 'viewField', + ); + + const viewId = v4(); + + const existingViews = await viewRepository.find({ + where: { + objectMetadataId: objectMetadata.id, + name: viewName, + }, + }); + + if (existingViews.length > 0) { + this.logger.log(`View already exists: ${existingViews[0].id}`); + + return existingViews[0].id; + } + + if (dryRun) { + this.logger.log(`Dry run: Creating view: ${viewName}`); + + return viewId; + } + + await viewRepository.insert({ + id: viewId, + name: viewName, + objectMetadataId: objectMetadata.id, + type: 'table', + key: 'INDEX', + position: 0, + icon: 'IconSettingsAutomation', + kanbanFieldMetadataId: '', + }); + + await viewFieldRepository.insert({ + fieldMetadataId: fieldMetadataName.id, + position: 0, + isVisible: true, + size: 210, + viewId: viewId, + }); + + return viewId; + } +} diff --git a/packages/twenty-server/src/modules/workflow/common/commands/workflow-command.module.ts b/packages/twenty-server/src/modules/workflow/common/commands/workflow-command.module.ts new file mode 100644 index 000000000000..08389b824d7f --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/common/commands/workflow-command.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { SeedWorkflowViewsCommand } from 'src/modules/workflow/common/commands/seed-workflow-views.command'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([Workspace], 'core'), + TypeOrmModule.forFeature( + [ObjectMetadataEntity, FieldMetadataEntity], + 'metadata', + ), + ], + providers: [SeedWorkflowViewsCommand], + exports: [SeedWorkflowViewsCommand], +}) +export class WorkflowCommandModule {} diff --git a/packages/twenty-server/src/modules/workflow/common/workflow-common.module.ts b/packages/twenty-server/src/modules/workflow/common/workflow-common.module.ts index fc23aa8ff88a..e49d85465de4 100644 --- a/packages/twenty-server/src/modules/workflow/common/workflow-common.module.ts +++ b/packages/twenty-server/src/modules/workflow/common/workflow-common.module.ts @@ -1,10 +1,11 @@ import { Module } from '@nestjs/common'; +import { WorkflowCommandModule } from 'src/modules/workflow/common/commands/workflow-command.module'; import { WorkflowQueryHookModule } from 'src/modules/workflow/common/query-hooks/workflow-query-hook.module'; import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; @Module({ - imports: [WorkflowQueryHookModule], + imports: [WorkflowQueryHookModule, WorkflowCommandModule], providers: [WorkflowCommonWorkspaceService], exports: [WorkflowCommonWorkspaceService], })