diff --git a/packages/twenty-server/@types/express.d.ts b/packages/twenty-server/@types/express.d.ts index 1e45cf88d079..f30a99a198d7 100644 --- a/packages/twenty-server/@types/express.d.ts +++ b/packages/twenty-server/@types/express.d.ts @@ -9,5 +9,6 @@ declare module 'express-serve-static-core' { workspace?: Workspace; workspaceId?: string; workspaceMetadataVersion?: number; + workspaceMemberId?: string; } } diff --git a/packages/twenty-server/src/engine/core-modules/actor/actor.module.ts b/packages/twenty-server/src/engine/core-modules/actor/actor.module.ts new file mode 100644 index 000000000000..d69cc69a23f4 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/actor/actor.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { CreatedByPreQueryHook } from 'src/engine/core-modules/actor/query-hooks/created-by.pre-query-hook'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([FieldMetadataEntity], 'metadata')], + providers: [CreatedByPreQueryHook], + exports: [CreatedByPreQueryHook], +}) +export class ActorModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/query-hooks/created-by.pre-query-hook.ts b/packages/twenty-server/src/engine/core-modules/actor/query-hooks/created-by.pre-query-hook.ts similarity index 84% rename from packages/twenty-server/src/engine/metadata-modules/field-metadata/query-hooks/created-by.pre-query-hook.ts rename to packages/twenty-server/src/engine/core-modules/actor/query-hooks/created-by.pre-query-hook.ts index 1e33aa850510..2a90843f1d69 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/query-hooks/created-by.pre-query-hook.ts +++ b/packages/twenty-server/src/engine/core-modules/actor/query-hooks/created-by.pre-query-hook.ts @@ -1,3 +1,4 @@ +import { Logger } from '@nestjs/common/services/logger.service'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; @@ -6,6 +7,7 @@ import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-que import { CreateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator'; +import { buildCreatedByFromWorkspaceMember } from 'src/engine/core-modules/actor/utils/build-created-by-from-workspace-member.util'; import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; import { ActorMetadata, @@ -26,6 +28,8 @@ type CustomWorkspaceItem = Omit< @WorkspaceQueryHook(`*.createMany`) export class CreatedByPreQueryHook implements WorkspaceQueryHookInstance { + private readonly logger = new Logger(CreatedByPreQueryHook.name); + constructor( private readonly twentyORMGlobalManager: TwentyORMGlobalManager, @InjectRepository(FieldMetadataEntity, 'metadata') @@ -55,7 +59,14 @@ export class CreatedByPreQueryHook implements WorkspaceQueryHookInstance { } // If user is logged in, we use the workspace member - if (authContext.user) { + if (authContext.workspaceMemberId && authContext.user) { + createdBy = buildCreatedByFromWorkspaceMember( + authContext.workspaceMemberId, + authContext.user, + ); + // TODO: remove that code once we have the workspace member id in all tokens + } else if (authContext.user) { + this.logger.warn("User doesn't have a workspace member id in the token"); const workspaceMemberRepository = await this.twentyORMGlobalManager.getRepositoryForWorkspace( authContext.workspace.id, diff --git a/packages/twenty-server/src/engine/core-modules/actor/utils/build-created-by-from-workspace-member.util.ts b/packages/twenty-server/src/engine/core-modules/actor/utils/build-created-by-from-workspace-member.util.ts new file mode 100644 index 000000000000..a553f9970c60 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/actor/utils/build-created-by-from-workspace-member.util.ts @@ -0,0 +1,11 @@ +import { User } from 'src/engine/core-modules/user/user.entity'; +import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; + +export const buildCreatedByFromWorkspaceMember = ( + workspaceMemberId: string, + user: User, +) => ({ + workspaceMemberId, + source: FieldActorSource.MANUAL, + name: `${user.firstName} ${user.lastName}`, +}); diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/token.service.spec.ts b/packages/twenty-server/src/engine/core-modules/auth/services/token.service.spec.ts index 0d807db41bff..29e2df741b82 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/token.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/token.service.spec.ts @@ -16,6 +16,7 @@ import { User } from 'src/engine/core-modules/user/user.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { EmailService } from 'src/engine/integrations/email/email.service'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { TokenService } from './token.service'; @@ -66,6 +67,10 @@ describe('TokenService', () => { provide: getRepositoryToken(Workspace, 'core'), useValue: {}, }, + { + provide: TwentyORMGlobalManager, + useValue: {}, + }, ], }).compile(); diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/token.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/token.service.ts index da571fbe5184..200430de73b0 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/token.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/token.service.ts @@ -41,6 +41,8 @@ import { User } from 'src/engine/core-modules/user/user.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { EmailService } from 'src/engine/integrations/email/email.service'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @Injectable() export class TokenService { @@ -55,6 +57,7 @@ export class TokenService { @InjectRepository(Workspace, 'core') private readonly workspaceRepository: Repository, private readonly emailService: EmailService, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} async generateAccessToken( @@ -91,9 +94,33 @@ export class TokenService { ); } + const workspaceIdNonNullable = workspaceId + ? workspaceId + : user.defaultWorkspace.id; + + const workspaceMemberRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceIdNonNullable, + 'workspaceMember', + ); + + const workspaceMember = await workspaceMemberRepository.findOne({ + where: { + userId: user.id, + }, + }); + + if (!workspaceMember) { + throw new AuthException( + 'User is not a member of the workspace', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); + } + const jwtPayload: JwtPayload = { sub: user.id, workspaceId: workspaceId ? workspaceId : user.defaultWorkspace.id, + workspaceMemberId: workspaceMember.id, }; return { @@ -247,11 +274,10 @@ export class TokenService { this.environmentService.get('ACCESS_TOKEN_SECRET'), ); - const { user, apiKey, workspace } = await this.jwtStrategy.validate( - decoded as JwtPayload, - ); + const { user, apiKey, workspace, workspaceMemberId } = + await this.jwtStrategy.validate(decoded as JwtPayload); - return { user, apiKey, workspace }; + return { user, apiKey, workspace, workspaceMemberId }; } async verifyLoginToken(loginToken: string): Promise { diff --git a/packages/twenty-server/src/engine/core-modules/auth/strategies/jwt.auth.strategy.ts b/packages/twenty-server/src/engine/core-modules/auth/strategies/jwt.auth.strategy.ts index 2807232fc9c6..5764e967815f 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/strategies/jwt.auth.strategy.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/strategies/jwt.auth.strategy.ts @@ -17,7 +17,12 @@ import { EnvironmentService } from 'src/engine/integrations/environment/environm import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { ApiKeyWorkspaceEntity } from 'src/modules/api-key/standard-objects/api-key.workspace-entity'; -export type JwtPayload = { sub: string; workspaceId: string; jti?: string }; +export type JwtPayload = { + sub: string; + workspaceId: string; + workspaceMemberId: string; + jti?: string; +}; @Injectable() export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') { @@ -95,6 +100,9 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') { } } - return { user, apiKey, workspace }; + // We don't check if the user is a member of the workspace yet + const workspaceMemberId = payload.workspaceMemberId; + + return { user, apiKey, workspace, workspaceMemberId }; } } diff --git a/packages/twenty-server/src/engine/core-modules/auth/types/auth-context.type.ts b/packages/twenty-server/src/engine/core-modules/auth/types/auth-context.type.ts index cf8faca181bc..80c223f4e926 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/types/auth-context.type.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/types/auth-context.type.ts @@ -5,5 +5,6 @@ import { ApiKeyWorkspaceEntity } from 'src/modules/api-key/standard-objects/api- export type AuthContext = { user?: User | null | undefined; apiKey?: ApiKeyWorkspaceEntity | null | undefined; + workspaceMemberId?: string; workspace: Workspace; }; diff --git a/packages/twenty-server/src/engine/core-modules/core-engine.module.ts b/packages/twenty-server/src/engine/core-modules/core-engine.module.ts index 37b9ac882052..27312a15159d 100644 --- a/packages/twenty-server/src/engine/core-modules/core-engine.module.ts +++ b/packages/twenty-server/src/engine/core-modules/core-engine.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; +import { ActorModule } from 'src/engine/core-modules/actor/actor.module'; import { AISQLQueryModule } from 'src/engine/core-modules/ai-sql-query/ai-sql-query.module'; import { AppTokenModule } from 'src/engine/core-modules/app-token/app-token.module'; import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; @@ -11,7 +12,7 @@ import { TimelineMessagingModule } from 'src/engine/core-modules/messaging/timel import { OpenApiModule } from 'src/engine/core-modules/open-api/open-api.module'; import { PostgresCredentialsModule } from 'src/engine/core-modules/postgres-credentials/postgres-credentials.module'; import { UserModule } from 'src/engine/core-modules/user/user.module'; -import { WorkflowTriggerCoreModule } from 'src/engine/core-modules/workflow/core-workflow-trigger.module'; +import { WorkflowTriggerApiModule } from 'src/engine/core-modules/workflow/workflow-trigger-api.module'; import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module'; import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module'; @@ -36,8 +37,9 @@ import { FileModule } from './file/file.module'; WorkspaceModule, AISQLQueryModule, PostgresCredentialsModule, - WorkflowTriggerCoreModule, + WorkflowTriggerApiModule, WorkspaceEventEmitterModule, + ActorModule, ], exports: [ AnalyticsModule, diff --git a/packages/twenty-server/src/engine/core-modules/workflow/core-workflow-trigger.module.ts b/packages/twenty-server/src/engine/core-modules/workflow/core-workflow-trigger.module.ts deleted file mode 100644 index 76cefa494ab1..000000000000 --- a/packages/twenty-server/src/engine/core-modules/workflow/core-workflow-trigger.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { WorkflowTriggerResolver } from 'src/engine/core-modules/workflow/workflow-trigger.resolver'; -import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module'; -import { WorkflowRunnerModule } from 'src/modules/workflow/workflow-runner/workflow-runner.module'; -import { WorkflowTriggerWorkspaceService } from 'src/modules/workflow/workflow-trigger/workflow-trigger.workspace-service'; - -@Module({ - imports: [WorkflowCommonModule, WorkflowRunnerModule], - providers: [WorkflowTriggerWorkspaceService, WorkflowTriggerResolver], -}) -export class WorkflowTriggerCoreModule {} diff --git a/packages/twenty-server/src/engine/core-modules/workflow/dtos/workflow-run.dto.ts b/packages/twenty-server/src/engine/core-modules/workflow/dtos/workflow-run.dto.ts new file mode 100644 index 000000000000..7516eca15e46 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/workflow/dtos/workflow-run.dto.ts @@ -0,0 +1,9 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; + +@ObjectType('WorkflowRun') +export class WorkflowRunDTO { + @Field(() => UUIDScalarType) + workflowRunId: string; +} diff --git a/packages/twenty-server/src/engine/core-modules/workflow/dtos/workflow-trigger-result.dto.ts b/packages/twenty-server/src/engine/core-modules/workflow/dtos/workflow-trigger-result.dto.ts deleted file mode 100644 index d5433e5b1a7e..000000000000 --- a/packages/twenty-server/src/engine/core-modules/workflow/dtos/workflow-trigger-result.dto.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Field, ObjectType } from '@nestjs/graphql'; - -import { IsObject } from 'class-validator'; -import graphqlTypeJson from 'graphql-type-json'; - -@ObjectType('WorkflowTriggerResult') -export class WorkflowTriggerResultDTO { - @IsObject() - @Field(() => graphqlTypeJson, { - description: 'Execution result in JSON format', - nullable: true, - }) - result?: JSON; -} diff --git a/packages/twenty-server/src/engine/core-modules/workflow/utils/workflow-trigger-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/core-modules/workflow/filters/workflow-trigger-graphql-api-exception.filter.ts similarity index 59% rename from packages/twenty-server/src/engine/core-modules/workflow/utils/workflow-trigger-graphql-api-exception-handler.util.ts rename to packages/twenty-server/src/engine/core-modules/workflow/filters/workflow-trigger-graphql-api-exception.filter.ts index e3ac10a136c5..13edf52a99ee 100644 --- a/packages/twenty-server/src/engine/core-modules/workflow/utils/workflow-trigger-graphql-api-exception-handler.util.ts +++ b/packages/twenty-server/src/engine/core-modules/workflow/filters/workflow-trigger-graphql-api-exception.filter.ts @@ -1,3 +1,5 @@ +import { Catch, ExceptionFilter } from '@nestjs/common'; + import { InternalServerError, UserInputError, @@ -7,18 +9,19 @@ import { WorkflowTriggerExceptionCode, } from 'src/modules/workflow/workflow-trigger/workflow-trigger.exception'; -export const workflowTriggerGraphqlApiExceptionHandler = (error: Error) => { - if (error instanceof WorkflowTriggerException) { - switch (error.code) { +@Catch(WorkflowTriggerException) +export class WorkflowTriggerGraphqlApiExceptionFilter + implements ExceptionFilter +{ + catch(exception: WorkflowTriggerException) { + switch (exception.code) { case WorkflowTriggerExceptionCode.INVALID_INPUT: - throw new UserInputError(error.message); + throw new UserInputError(exception.message); case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER: case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION: case WorkflowTriggerExceptionCode.INVALID_ACTION_TYPE: default: - throw new InternalServerError(error.message); + throw new InternalServerError(exception.message); } } - - throw error; -}; +} diff --git a/packages/twenty-server/src/engine/core-modules/workflow/workflow-trigger-api.module.ts b/packages/twenty-server/src/engine/core-modules/workflow/workflow-trigger-api.module.ts new file mode 100644 index 000000000000..8c469833ce29 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/workflow/workflow-trigger-api.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; + +import { WorkflowTriggerResolver } from 'src/engine/core-modules/workflow/workflow-trigger.resolver'; +import { WorkflowTriggerModule } from 'src/modules/workflow/workflow-trigger/workflow-trigger.module'; + +@Module({ + imports: [WorkflowTriggerModule], + providers: [WorkflowTriggerResolver], +}) +export class WorkflowTriggerApiModule {} diff --git a/packages/twenty-server/src/engine/core-modules/workflow/workflow-trigger.resolver.ts b/packages/twenty-server/src/engine/core-modules/workflow/workflow-trigger.resolver.ts index 35c8a40882ba..cdce3b954a9b 100644 --- a/packages/twenty-server/src/engine/core-modules/workflow/workflow-trigger.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workflow/workflow-trigger.resolver.ts @@ -1,14 +1,18 @@ -import { UseGuards } from '@nestjs/common'; +import { UseFilters, UseGuards } from '@nestjs/common'; import { Args, Mutation, Resolver } from '@nestjs/graphql'; +import { User } from 'src/engine/core-modules/user/user.entity'; import { RunWorkflowVersionInput } from 'src/engine/core-modules/workflow/dtos/run-workflow-version-input.dto'; -import { WorkflowTriggerResultDTO } from 'src/engine/core-modules/workflow/dtos/workflow-trigger-result.dto'; -import { workflowTriggerGraphqlApiExceptionHandler } from 'src/engine/core-modules/workflow/utils/workflow-trigger-graphql-api-exception-handler.util'; +import { WorkflowRunDTO } from 'src/engine/core-modules/workflow/dtos/workflow-run.dto'; +import { WorkflowTriggerGraphqlApiExceptionFilter } from 'src/engine/core-modules/workflow/filters/workflow-trigger-graphql-api-exception.filter'; +import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator'; +import { AuthWorkspaceMemberId } from 'src/engine/decorators/auth/auth-workspace-member-id.decorator'; import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; import { WorkflowTriggerWorkspaceService } from 'src/modules/workflow/workflow-trigger/workflow-trigger.workspace-service'; -@UseGuards(JwtAuthGuard) @Resolver() +@UseGuards(JwtAuthGuard) +@UseFilters(WorkflowTriggerGraphqlApiExceptionFilter) export class WorkflowTriggerResolver { constructor( private readonly workflowTriggerWorkspaceService: WorkflowTriggerWorkspaceService, @@ -18,28 +22,22 @@ export class WorkflowTriggerResolver { async enableWorkflowTrigger( @Args('workflowVersionId') workflowVersionId: string, ) { - try { - return await this.workflowTriggerWorkspaceService.enableWorkflowTrigger( - workflowVersionId, - ); - } catch (error) { - workflowTriggerGraphqlApiExceptionHandler(error); - } + return await this.workflowTriggerWorkspaceService.enableWorkflowTrigger( + workflowVersionId, + ); } - @Mutation(() => WorkflowTriggerResultDTO) + @Mutation(() => WorkflowRunDTO) async runWorkflowVersion( + @AuthWorkspaceMemberId() workspaceMemberId: string, + @AuthUser() user: User, @Args('input') { workflowVersionId, payload }: RunWorkflowVersionInput, ) { - try { - return { - result: await this.workflowTriggerWorkspaceService.runWorkflowVersion( - workflowVersionId, - payload ?? {}, - ), - }; - } catch (error) { - workflowTriggerGraphqlApiExceptionHandler(error); - } + return await this.workflowTriggerWorkspaceService.runWorkflowVersion( + workflowVersionId, + payload ?? {}, + workspaceMemberId, + user, + ); } } diff --git a/packages/twenty-server/src/engine/decorators/auth/auth-workspace-member-id.decorator.ts b/packages/twenty-server/src/engine/decorators/auth/auth-workspace-member-id.decorator.ts new file mode 100644 index 000000000000..17900a327c7a --- /dev/null +++ b/packages/twenty-server/src/engine/decorators/auth/auth-workspace-member-id.decorator.ts @@ -0,0 +1,11 @@ +import { ExecutionContext, createParamDecorator } from '@nestjs/common'; + +import { getRequest } from 'src/utils/extract-request'; + +export const AuthWorkspaceMemberId = createParamDecorator( + (data: unknown, ctx: ExecutionContext) => { + const request = getRequest(ctx); + + return request.workspaceMemberId; + }, +); diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts index c8797101d6b6..933729c437be 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts @@ -8,12 +8,12 @@ import { import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; +import { ActorModule } from 'src/engine/core-modules/actor/actor.module'; import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; import { FieldMetadataResolver } from 'src/engine/metadata-modules/field-metadata/field-metadata.resolver'; import { FieldMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/field-metadata/interceptors/field-metadata-graphql-api-exception.interceptor'; -import { CreatedByPreQueryHook } from 'src/engine/metadata-modules/field-metadata/query-hooks/created-by.pre-query-hook'; import { IsFieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/validators/is-field-metadata-default-value.validator'; import { IsFieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/validators/is-field-metadata-options.validator'; import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; @@ -42,12 +42,9 @@ import { UpdateFieldInput } from './dtos/update-field.input'; ObjectMetadataModule, DataSourceModule, TypeORMModule, + ActorModule, ], - services: [ - IsFieldMetadataDefaultValue, - FieldMetadataService, - CreatedByPreQueryHook, - ], + services: [IsFieldMetadataDefaultValue, FieldMetadataService], resolvers: [ { EntityClass: FieldMetadataEntity, diff --git a/packages/twenty-server/src/engine/middlewares/graphql-hydrate-request-from-token.middleware.ts b/packages/twenty-server/src/engine/middlewares/graphql-hydrate-request-from-token.middleware.ts index 509e58bb31c7..c510f3e4a3b7 100644 --- a/packages/twenty-server/src/engine/middlewares/graphql-hydrate-request-from-token.middleware.ts +++ b/packages/twenty-server/src/engine/middlewares/graphql-hydrate-request-from-token.middleware.ts @@ -82,6 +82,7 @@ export class GraphQLHydrateRequestFromTokenMiddleware req.workspace = data.workspace; req.workspaceId = data.workspace.id; req.workspaceMetadataVersion = metadataVersion; + req.workspaceMemberId = data.workspaceMemberId; } catch (error) { res.writeHead(200, { 'Content-Type': 'application/json' }); res.write( diff --git a/packages/twenty-server/src/modules/workflow/workflow-action-executor/workflow-action-executor.exception.ts b/packages/twenty-server/src/modules/workflow/workflow-action-executor/workflow-action-executor.exception.ts new file mode 100644 index 000000000000..83d6150586df --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-action-executor/workflow-action-executor.exception.ts @@ -0,0 +1,12 @@ +import { CustomException } from 'src/utils/custom-exception'; + +export class WorkflowActionExecutorException extends CustomException { + code: WorkflowActionExecutorExceptionCode; + constructor(message: string, code: WorkflowActionExecutorExceptionCode) { + super(message, code); + } +} + +export enum WorkflowActionExecutorExceptionCode { + SCOPED_WORKSPACE_NOT_FOUND = 'SCOPED_WORKSPACE_NOT_FOUND', +} diff --git a/packages/twenty-server/src/modules/workflow/workflow-action-executor/workflow-action-executor.factory.ts b/packages/twenty-server/src/modules/workflow/workflow-action-executor/workflow-action-executor.factory.ts new file mode 100644 index 000000000000..485bb8133235 --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-action-executor/workflow-action-executor.factory.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@nestjs/common'; + +import { WorkflowActionType } from 'src/modules/workflow/common/types/workflow-action.type'; +import { WorkflowActionExecutor } from 'src/modules/workflow/workflow-action-executor/workflow-action-executor.interface'; +import { CodeWorkflowActionExecutor } from 'src/modules/workflow/workflow-action-executor/workflow-action-executors/code-workflow-action-executor'; + +@Injectable() +export class WorkflowActionExecutorFactory { + constructor( + private readonly codeWorkflowActionExecutor: CodeWorkflowActionExecutor, + ) {} + + get(actionType: WorkflowActionType): WorkflowActionExecutor { + switch (actionType) { + case WorkflowActionType.CODE: + return this.codeWorkflowActionExecutor; + default: + throw new Error( + `Workflow action executor not found for action type '${actionType}'`, + ); + } + } +} diff --git a/packages/twenty-server/src/modules/workflow/workflow-action-runner/workflow-action-runner.interface.ts b/packages/twenty-server/src/modules/workflow/workflow-action-executor/workflow-action-executor.interface.ts similarity index 87% rename from packages/twenty-server/src/modules/workflow/workflow-action-runner/workflow-action-runner.interface.ts rename to packages/twenty-server/src/modules/workflow/workflow-action-executor/workflow-action-executor.interface.ts index 1591af06c027..ea2adfdcf03b 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-action-runner/workflow-action-runner.interface.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-action-executor/workflow-action-executor.interface.ts @@ -1,7 +1,7 @@ import { WorkflowAction } from 'src/modules/workflow/common/types/workflow-action.type'; import { WorkflowResult } from 'src/modules/workflow/common/types/workflow-result.type'; -export interface WorkflowActionRunner { +export interface WorkflowActionExecutor { execute({ action, payload, diff --git a/packages/twenty-server/src/modules/workflow/workflow-action-executor/workflow-action-executor.module.ts b/packages/twenty-server/src/modules/workflow/workflow-action-executor/workflow-action-executor.module.ts new file mode 100644 index 000000000000..c827d1a05f54 --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-action-executor/workflow-action-executor.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; + +import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module'; +import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory'; +import { WorkflowActionExecutorFactory } from 'src/modules/workflow/workflow-action-executor/workflow-action-executor.factory'; +import { CodeWorkflowActionExecutor } from 'src/modules/workflow/workflow-action-executor/workflow-action-executors/code-workflow-action-executor'; + +@Module({ + imports: [ServerlessFunctionModule], + providers: [ + WorkflowActionExecutorFactory, + CodeWorkflowActionExecutor, + ScopedWorkspaceContextFactory, + ], + exports: [WorkflowActionExecutorFactory], +}) +export class WorkflowActionExecutorModule {} diff --git a/packages/twenty-server/src/modules/workflow/workflow-action-runner/workflow-action-runners/code-workflow-action-runner.ts b/packages/twenty-server/src/modules/workflow/workflow-action-executor/workflow-action-executors/code-workflow-action-executor.ts similarity index 71% rename from packages/twenty-server/src/modules/workflow/workflow-action-runner/workflow-action-runners/code-workflow-action-runner.ts rename to packages/twenty-server/src/modules/workflow/workflow-action-executor/workflow-action-executors/code-workflow-action-executor.ts index 2afedcb21402..cd11dedbf967 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-action-runner/workflow-action-runners/code-workflow-action-runner.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-action-executor/workflow-action-executors/code-workflow-action-executor.ts @@ -5,13 +5,13 @@ import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/s import { WorkflowAction } from 'src/modules/workflow/common/types/workflow-action.type'; import { WorkflowResult } from 'src/modules/workflow/common/types/workflow-result.type'; import { - WorkflowActionRunnerException, - WorkflowActionRunnerExceptionCode, -} from 'src/modules/workflow/workflow-action-runner/workflow-action-runner.exception'; -import { WorkflowActionRunner } from 'src/modules/workflow/workflow-action-runner/workflow-action-runner.interface'; + WorkflowActionExecutorException, + WorkflowActionExecutorExceptionCode, +} from 'src/modules/workflow/workflow-action-executor/workflow-action-executor.exception'; +import { WorkflowActionExecutor } from 'src/modules/workflow/workflow-action-executor/workflow-action-executor.interface'; @Injectable() -export class CodeWorkflowActionRunner implements WorkflowActionRunner { +export class CodeWorkflowActionExecutor implements WorkflowActionExecutor { constructor( private readonly serverlessFunctionService: ServerlessFunctionService, private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory, @@ -27,9 +27,9 @@ export class CodeWorkflowActionRunner implements WorkflowActionRunner { const { workspaceId } = this.scopedWorkspaceContextFactory.create(); if (!workspaceId) { - throw new WorkflowActionRunnerException( + throw new WorkflowActionExecutorException( 'Scoped workspace not found', - WorkflowActionRunnerExceptionCode.SCOPED_WORKSPACE_NOT_FOUND, + WorkflowActionExecutorExceptionCode.SCOPED_WORKSPACE_NOT_FOUND, ); } diff --git a/packages/twenty-server/src/modules/workflow/workflow-action-runner/workflow-action-runner.exception.ts b/packages/twenty-server/src/modules/workflow/workflow-action-runner/workflow-action-runner.exception.ts deleted file mode 100644 index 9284ce454dab..000000000000 --- a/packages/twenty-server/src/modules/workflow/workflow-action-runner/workflow-action-runner.exception.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { CustomException } from 'src/utils/custom-exception'; - -export class WorkflowActionRunnerException extends CustomException { - code: WorkflowActionRunnerExceptionCode; - constructor(message: string, code: WorkflowActionRunnerExceptionCode) { - super(message, code); - } -} - -export enum WorkflowActionRunnerExceptionCode { - SCOPED_WORKSPACE_NOT_FOUND = 'SCOPED_WORKSPACE_NOT_FOUND', -} diff --git a/packages/twenty-server/src/modules/workflow/workflow-action-runner/workflow-action-runner.factory.ts b/packages/twenty-server/src/modules/workflow/workflow-action-runner/workflow-action-runner.factory.ts deleted file mode 100644 index 5ab4d2a42fe6..000000000000 --- a/packages/twenty-server/src/modules/workflow/workflow-action-runner/workflow-action-runner.factory.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { CodeWorkflowActionRunner } from 'src/modules/workflow/workflow-action-runner/workflow-action-runners/code-workflow-action-runner'; -import { WorkflowActionType } from 'src/modules/workflow/common/types/workflow-action.type'; -import { WorkflowActionRunner } from 'src/modules/workflow/workflow-action-runner/workflow-action-runner.interface'; - -@Injectable() -export class WorkflowActionRunnerFactory { - constructor( - private readonly codeWorkflowActionRunner: CodeWorkflowActionRunner, - ) {} - - get(actionType: WorkflowActionType): WorkflowActionRunner { - switch (actionType) { - case WorkflowActionType.CODE: - return this.codeWorkflowActionRunner; - default: - throw new Error( - `Workflow action executor not found for action type '${actionType}'`, - ); - } - } -} diff --git a/packages/twenty-server/src/modules/workflow/workflow-action-runner/workflow-action-runner.module.ts b/packages/twenty-server/src/modules/workflow/workflow-action-runner/workflow-action-runner.module.ts deleted file mode 100644 index 1bafd6cfe396..000000000000 --- a/packages/twenty-server/src/modules/workflow/workflow-action-runner/workflow-action-runner.module.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module'; -import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory'; -import { WorkflowActionRunnerFactory } from 'src/modules/workflow/workflow-action-runner/workflow-action-runner.factory'; -import { CodeWorkflowActionRunner } from 'src/modules/workflow/workflow-action-runner/workflow-action-runners/code-workflow-action-runner'; - -@Module({ - imports: [ServerlessFunctionModule], - providers: [ - WorkflowActionRunnerFactory, - CodeWorkflowActionRunner, - ScopedWorkspaceContextFactory, - ], - exports: [WorkflowActionRunnerFactory], -}) -export class WorkflowActionRunnerModule {} diff --git a/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-runner.exception.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-executor.exception.ts similarity index 62% rename from packages/twenty-server/src/modules/workflow/workflow-runner/workflow-runner.exception.ts rename to packages/twenty-server/src/modules/workflow/workflow-executor/workflow-executor.exception.ts index 1bab0da515ee..cdba717b8f67 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-runner.exception.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-executor.exception.ts @@ -1,11 +1,11 @@ import { CustomException } from 'src/utils/custom-exception'; -export class WorkflowRunnerException extends CustomException { +export class WorkflowExecutorException extends CustomException { constructor(message: string, code: string) { super(message, code); } } -export enum WorkflowRunnerExceptionCode { +export enum WorkflowExecutorExceptionCode { WORKFLOW_FAILED = 'WORKFLOW_FAILED', } diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-executor.module.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-executor.module.ts new file mode 100644 index 000000000000..43d9a6ad6af5 --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-executor.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; + +import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module'; +import { WorkflowActionExecutorModule } from 'src/modules/workflow/workflow-action-executor/workflow-action-executor.module'; +import { WorkflowExecutorWorkspaceService } from 'src/modules/workflow/workflow-executor/workflow-executor.workspace-service'; + +@Module({ + imports: [WorkflowCommonModule, WorkflowActionExecutorModule], + providers: [WorkflowExecutorWorkspaceService], + exports: [WorkflowExecutorWorkspaceService], +}) +export class WorkflowExecutorModule {} diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-executor.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-executor.workspace-service.ts new file mode 100644 index 000000000000..0408056865b1 --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-executor.workspace-service.ts @@ -0,0 +1,84 @@ +import { Injectable } from '@nestjs/common'; + +import { WorkflowAction } from 'src/modules/workflow/common/types/workflow-action.type'; +import { WorkflowActionExecutorFactory } from 'src/modules/workflow/workflow-action-executor/workflow-action-executor.factory'; +import { + WorkflowExecutorException, + WorkflowExecutorExceptionCode, +} from 'src/modules/workflow/workflow-executor/workflow-executor.exception'; + +const MAX_RETRIES_ON_FAILURE = 3; + +export type WorkflowExecutionOutput = { + data?: object; + error?: object; +}; + +@Injectable() +export class WorkflowExecutorWorkspaceService { + constructor( + private readonly workflowActionExecutorFactory: WorkflowActionExecutorFactory, + ) {} + + async execute({ + action, + payload, + attemptCount = 1, + }: { + action?: WorkflowAction; + payload?: object; + attemptCount?: number; + }): Promise { + if (!action) { + return { + data: payload, + }; + } + + const workflowActionExecutor = this.workflowActionExecutorFactory.get( + action.type, + ); + + const result = await workflowActionExecutor.execute({ + action, + payload, + }); + + if (result.data) { + return await this.execute({ + action: action.nextAction, + payload: result.data, + }); + } + + if (!result.error) { + throw new WorkflowExecutorException( + 'Execution result error, no data or error', + WorkflowExecutorExceptionCode.WORKFLOW_FAILED, + ); + } + + if (action.settings.errorHandlingOptions.continueOnFailure.value) { + return await this.execute({ + action: action.nextAction, + payload, + }); + } + + if ( + action.settings.errorHandlingOptions.retryOnFailure.value && + attemptCount < MAX_RETRIES_ON_FAILURE + ) { + return await this.execute({ + action, + payload, + attemptCount: attemptCount + 1, + }); + } + + throw new WorkflowExecutorException( + `Workflow failed: ${result.error}`, + WorkflowExecutorExceptionCode.WORKFLOW_FAILED, + ); + } +} diff --git a/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-runner.job.ts b/packages/twenty-server/src/modules/workflow/workflow-runner/jobs/run-workflow.job.ts similarity index 65% rename from packages/twenty-server/src/modules/workflow/workflow-runner/workflow-runner.job.ts rename to packages/twenty-server/src/modules/workflow/workflow-runner/jobs/run-workflow.job.ts index 64f1d6984fa5..619d4babffd7 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-runner.job.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-runner/jobs/run-workflow.job.ts @@ -5,8 +5,8 @@ import { Processor } from 'src/engine/integrations/message-queue/decorators/proc import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; import { WorkflowRunStatus } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity'; import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workflow-common.workspace-service'; -import { WorkflowRunnerWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-runner.workspace-service'; -import { WorkflowStatusWorkspaceService } from 'src/modules/workflow/workflow-status/workflow-status.workspace-service'; +import { WorkflowExecutorWorkspaceService } from 'src/modules/workflow/workflow-executor/workflow-executor.workspace-service'; +import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service'; export type RunWorkflowJobData = { workspaceId: string; @@ -16,20 +16,20 @@ export type RunWorkflowJobData = { }; @Processor({ queueName: MessageQueue.workflowQueue, scope: Scope.REQUEST }) -export class WorkflowRunnerJob { +export class RunWorkflowJob { constructor( private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService, - private readonly workflowRunnerWorkspaceService: WorkflowRunnerWorkspaceService, - private readonly workflowStatusWorkspaceService: WorkflowStatusWorkspaceService, + private readonly workflowExecutorWorkspaceService: WorkflowExecutorWorkspaceService, + private readonly workflowRunWorkspaceService: WorkflowRunWorkspaceService, ) {} - @Process(WorkflowRunnerJob.name) + @Process(RunWorkflowJob.name) async handle({ workflowVersionId, workflowRunId, payload, }: RunWorkflowJobData): Promise { - await this.workflowStatusWorkspaceService.startWorkflowRun(workflowRunId); + await this.workflowRunWorkspaceService.startWorkflowRun(workflowRunId); const workflowVersion = await this.workflowCommonWorkspaceService.getWorkflowVersion( @@ -37,17 +37,17 @@ export class WorkflowRunnerJob { ); try { - await this.workflowRunnerWorkspaceService.run({ + await this.workflowExecutorWorkspaceService.execute({ action: workflowVersion.trigger.nextAction, payload, }); - await this.workflowStatusWorkspaceService.endWorkflowRun( + await this.workflowRunWorkspaceService.endWorkflowRun( workflowRunId, WorkflowRunStatus.COMPLETED, ); } catch (error) { - await this.workflowStatusWorkspaceService.endWorkflowRun( + await this.workflowRunWorkspaceService.endWorkflowRun( workflowRunId, WorkflowRunStatus.FAILED, ); diff --git a/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run/workflow-run.exception.ts b/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run/workflow-run.exception.ts new file mode 100644 index 000000000000..e1668045ac0a --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run/workflow-run.exception.ts @@ -0,0 +1,13 @@ +import { CustomException } from 'src/utils/custom-exception'; + +export class WorkflowRunException extends CustomException { + code: WorkflowRunExceptionCode; + constructor(message: string, code: WorkflowRunExceptionCode) { + super(message, code); + } +} + +export enum WorkflowRunExceptionCode { + WORKFLOW_RUN_NOT_FOUND = 'WORKFLOW_RUN_NOT_FOUND', + INVALID_OPERATION = 'INVALID_OPERATION', +} diff --git a/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run/workflow-run.module.ts b/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run/workflow-run.module.ts new file mode 100644 index 000000000000..27ec554daf5f --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run/workflow-run.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; + +import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service'; + +@Module({ + providers: [WorkflowRunWorkspaceService], + exports: [WorkflowRunWorkspaceService], +}) +export class WorkflowRunModule {} diff --git a/packages/twenty-server/src/modules/workflow/workflow-status/workflow-status.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service.ts similarity index 79% rename from packages/twenty-server/src/modules/workflow/workflow-status/workflow-status.workspace-service.ts rename to packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service.ts index 949ae0c27e21..a772d9a6fb28 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-status/workflow-status.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service.ts @@ -7,12 +7,12 @@ import { WorkflowRunWorkspaceEntity, } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity'; import { - WorkflowStatusException, - WorkflowStatusExceptionCode, -} from 'src/modules/workflow/workflow-status/workflow-status.exception'; + WorkflowRunException, + WorkflowRunExceptionCode, +} from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.exception'; @Injectable() -export class WorkflowStatusWorkspaceService { +export class WorkflowRunWorkspaceService { constructor(private readonly twentyORMManager: TwentyORMManager) {} async createWorkflowRun(workflowVersionId: string, createdBy: ActorMetadata) { @@ -41,16 +41,16 @@ export class WorkflowStatusWorkspaceService { }); if (!workflowRunToUpdate) { - throw new WorkflowStatusException( + throw new WorkflowRunException( 'No workflow run to start', - WorkflowStatusExceptionCode.WORKFLOW_RUN_NOT_FOUND, + WorkflowRunExceptionCode.WORKFLOW_RUN_NOT_FOUND, ); } if (workflowRunToUpdate.status !== WorkflowRunStatus.NOT_STARTED) { - throw new WorkflowStatusException( + throw new WorkflowRunException( 'Workflow run already started', - WorkflowStatusExceptionCode.INVALID_OPERATION, + WorkflowRunExceptionCode.INVALID_OPERATION, ); } @@ -71,16 +71,16 @@ export class WorkflowStatusWorkspaceService { }); if (!workflowRunToUpdate) { - throw new WorkflowStatusException( + throw new WorkflowRunException( 'No workflow run to end', - WorkflowStatusExceptionCode.WORKFLOW_RUN_NOT_FOUND, + WorkflowRunExceptionCode.WORKFLOW_RUN_NOT_FOUND, ); } if (workflowRunToUpdate.status !== WorkflowRunStatus.RUNNING) { - throw new WorkflowStatusException( + throw new WorkflowRunException( 'Workflow cannot be ended as it is not running', - WorkflowStatusExceptionCode.INVALID_OPERATION, + WorkflowRunExceptionCode.INVALID_OPERATION, ); } diff --git a/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-runner.module.ts b/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-runner.module.ts index 9bd7e69d3876..bf33e5b21f02 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-runner.module.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-runner.module.ts @@ -1,18 +1,14 @@ import { Module } from '@nestjs/common'; import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module'; -import { WorkflowActionRunnerModule } from 'src/modules/workflow/workflow-action-runner/workflow-action-runner.module'; -import { WorkflowRunnerJob } from 'src/modules/workflow/workflow-runner/workflow-runner.job'; +import { WorkflowExecutorModule } from 'src/modules/workflow/workflow-executor/workflow-executor.module'; +import { RunWorkflowJob } from 'src/modules/workflow/workflow-runner/jobs/run-workflow.job'; +import { WorkflowRunModule } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.module'; import { WorkflowRunnerWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-runner.workspace-service'; -import { WorkflowStatusModule } from 'src/modules/workflow/workflow-status/workflow-status.module'; @Module({ - imports: [ - WorkflowCommonModule, - WorkflowActionRunnerModule, - WorkflowStatusModule, - ], - providers: [WorkflowRunnerWorkspaceService, WorkflowRunnerJob], + imports: [WorkflowRunModule, WorkflowCommonModule, WorkflowExecutorModule], + providers: [WorkflowRunnerWorkspaceService, RunWorkflowJob], exports: [WorkflowRunnerWorkspaceService], }) export class WorkflowRunnerModule {} diff --git a/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-runner.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-runner.workspace-service.ts index 0822bc5d2f34..bc43bd1b5a0d 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-runner.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-runner.workspace-service.ts @@ -1,84 +1,45 @@ import { Injectable } from '@nestjs/common'; -import { WorkflowAction } from 'src/modules/workflow/common/types/workflow-action.type'; -import { WorkflowActionRunnerFactory } from 'src/modules/workflow/workflow-action-runner/workflow-action-runner.factory'; +import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator'; +import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; +import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service'; +import { ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; import { - WorkflowRunnerException, - WorkflowRunnerExceptionCode, -} from 'src/modules/workflow/workflow-runner/workflow-runner.exception'; - -const MAX_RETRIES_ON_FAILURE = 3; - -export type WorkflowRunOutput = { - data?: object; - error?: object; -}; + RunWorkflowJob, + RunWorkflowJobData, +} from 'src/modules/workflow/workflow-runner/jobs/run-workflow.job'; +import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service'; @Injectable() export class WorkflowRunnerWorkspaceService { constructor( - private readonly workflowActionRunnerFactory: WorkflowActionRunnerFactory, + private readonly workflowRunWorkspaceService: WorkflowRunWorkspaceService, + @InjectMessageQueue(MessageQueue.workflowQueue) + private readonly messageQueueService: MessageQueueService, ) {} - async run({ - action, - payload, - attemptCount = 1, - }: { - action?: WorkflowAction; - payload?: object; - attemptCount?: number; - }): Promise { - if (!action) { - return { - data: payload, - }; - } - - const workflowActionRunner = this.workflowActionRunnerFactory.get( - action.type, - ); - - const result = await workflowActionRunner.execute({ - action, - payload, - }); - - if (result.data) { - return await this.run({ - action: action.nextAction, - payload: result.data, - }); - } - - if (!result.error) { - throw new WorkflowRunnerException( - 'Execution result error, no data or error', - WorkflowRunnerExceptionCode.WORKFLOW_FAILED, + async run( + workspaceId: string, + workflowVersionId: string, + payload: object, + source: ActorMetadata, + ) { + const workflowRunId = + await this.workflowRunWorkspaceService.createWorkflowRun( + workflowVersionId, + source, ); - } - - if (action.settings.errorHandlingOptions.continueOnFailure.value) { - return await this.run({ - action: action.nextAction, - payload, - }); - } - if ( - action.settings.errorHandlingOptions.retryOnFailure.value && - attemptCount < MAX_RETRIES_ON_FAILURE - ) { - return await this.run({ - action, - payload, - attemptCount: attemptCount + 1, - }); - } - - throw new WorkflowRunnerException( - `Workflow failed: ${result.error}`, - WorkflowRunnerExceptionCode.WORKFLOW_FAILED, + await this.messageQueueService.add( + RunWorkflowJob.name, + { + workspaceId, + workflowVersionId, + payload: payload, + workflowRunId, + }, ); + + return { workflowRunId }; } } diff --git a/packages/twenty-server/src/modules/workflow/workflow-status/workflow-status.exception.ts b/packages/twenty-server/src/modules/workflow/workflow-status/workflow-status.exception.ts deleted file mode 100644 index 6510815f090c..000000000000 --- a/packages/twenty-server/src/modules/workflow/workflow-status/workflow-status.exception.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { CustomException } from 'src/utils/custom-exception'; - -export class WorkflowStatusException extends CustomException { - code: WorkflowStatusExceptionCode; - constructor(message: string, code: WorkflowStatusExceptionCode) { - super(message, code); - } -} - -export enum WorkflowStatusExceptionCode { - WORKFLOW_RUN_NOT_FOUND = 'WORKFLOW_RUN_NOT_FOUND', - INVALID_OPERATION = 'INVALID_OPERATION', -} diff --git a/packages/twenty-server/src/modules/workflow/workflow-status/workflow-status.module.ts b/packages/twenty-server/src/modules/workflow/workflow-status/workflow-status.module.ts deleted file mode 100644 index 14eec4fb10c7..000000000000 --- a/packages/twenty-server/src/modules/workflow/workflow-status/workflow-status.module.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { WorkflowStatusWorkspaceService } from 'src/modules/workflow/workflow-status/workflow-status.workspace-service'; - -@Module({ - providers: [WorkflowStatusWorkspaceService], - exports: [WorkflowStatusWorkspaceService], -}) -export class WorkflowStatusModule {} diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/jobs/workflow-event-trigger.job.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/jobs/workflow-event-trigger.job.ts index f96e91b14e4f..0137dd57a3cd 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/jobs/workflow-event-trigger.job.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/jobs/workflow-event-trigger.job.ts @@ -1,18 +1,16 @@ import { Scope } from '@nestjs/common'; -import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator'; import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; -import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service'; import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity'; +import { WorkflowRunnerWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-runner.workspace-service'; import { - RunWorkflowJobData, - WorkflowRunnerJob, -} from 'src/modules/workflow/workflow-runner/workflow-runner.job'; -import { WorkflowStatusWorkspaceService } from 'src/modules/workflow/workflow-status/workflow-status.workspace-service'; + WorkflowTriggerException, + WorkflowTriggerExceptionCode, +} from 'src/modules/workflow/workflow-trigger/workflow-trigger.exception'; export type WorkflowEventTriggerJobData = { workspaceId: string; @@ -23,10 +21,8 @@ export type WorkflowEventTriggerJobData = { @Processor({ queueName: MessageQueue.workflowQueue, scope: Scope.REQUEST }) export class WorkflowEventTriggerJob { constructor( - @InjectMessageQueue(MessageQueue.workflowQueue) - private readonly messageQueueService: MessageQueueService, private readonly twentyORMManager: TwentyORMManager, - private readonly workflowStatusWorkspaceService: WorkflowStatusWorkspaceService, + private readonly workflowRunnerWorkspaceService: WorkflowRunnerWorkspaceService, ) {} @Process(WorkflowEventTriggerJob.name) @@ -41,23 +37,20 @@ export class WorkflowEventTriggerJob { }); if (!workflow.publishedVersionId) { - throw new Error('Workflow has no published version'); - } - - const workflowRunId = - await this.workflowStatusWorkspaceService.createWorkflowRun( - workflow.publishedVersionId, - { - source: FieldActorSource.WORKFLOW, - name: workflow.name, - }, + throw new WorkflowTriggerException( + 'Workflow has no published version', + WorkflowTriggerExceptionCode.INTERNAL_ERROR, ); + } - this.messageQueueService.add(WorkflowRunnerJob.name, { - workspaceId: data.workspaceId, - workflowVersionId: workflow.publishedVersionId, - payload: data.payload, - workflowRunId, - }); + await this.workflowRunnerWorkspaceService.run( + data.workspaceId, + workflow.publishedVersionId, + data.payload, + { + source: FieldActorSource.WORKFLOW, + name: workflow.name, + }, + ); } } diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/jobs/workflow-trigger-job.module.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/jobs/workflow-trigger-job.module.ts deleted file mode 100644 index 4f03c12fd958..000000000000 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/jobs/workflow-trigger-job.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { WorkflowRunnerModule } from 'src/modules/workflow/workflow-runner/workflow-runner.module'; -import { WorkflowStatusModule } from 'src/modules/workflow/workflow-status/workflow-status.module'; -import { WorkflowEventTriggerJob } from 'src/modules/workflow/workflow-trigger/jobs/workflow-event-trigger.job'; - -@Module({ - imports: [WorkflowRunnerModule, WorkflowStatusModule], - providers: [WorkflowEventTriggerJob], -}) -export class WorkflowTriggerJobModule {} diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/listeners/workflow-trigger-listener.module.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/listeners/workflow-trigger-listener.module.ts deleted file mode 100644 index 0c8b8afec9c3..000000000000 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/listeners/workflow-trigger-listener.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; -import { DatabaseEventTriggerListener } from 'src/modules/workflow/workflow-trigger/listeners/database-event-trigger.listener'; - -@Module({ - imports: [FeatureFlagModule], - providers: [DatabaseEventTriggerListener], -}) -export class WorkflowTriggerListenerModule {} diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/workflow-trigger.module.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/workflow-trigger.module.ts index 6ffdefb0e13c..ca8a6c83d71b 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/workflow-trigger.module.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/workflow-trigger.module.ts @@ -1,9 +1,21 @@ import { Module } from '@nestjs/common'; -import { WorkflowTriggerJobModule } from 'src/modules/workflow/workflow-trigger/jobs/workflow-trigger-job.module'; -import { WorkflowTriggerListenerModule } from 'src/modules/workflow/workflow-trigger/listeners/workflow-trigger-listener.module'; +import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; +import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory'; +import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module'; +import { WorkflowRunnerModule } from 'src/modules/workflow/workflow-runner/workflow-runner.module'; +import { WorkflowEventTriggerJob } from 'src/modules/workflow/workflow-trigger/jobs/workflow-event-trigger.job'; +import { DatabaseEventTriggerListener } from 'src/modules/workflow/workflow-trigger/listeners/database-event-trigger.listener'; +import { WorkflowTriggerWorkspaceService } from 'src/modules/workflow/workflow-trigger/workflow-trigger.workspace-service'; @Module({ - imports: [WorkflowTriggerJobModule, WorkflowTriggerListenerModule], + imports: [WorkflowCommonModule, WorkflowRunnerModule, FeatureFlagModule], + providers: [ + WorkflowTriggerWorkspaceService, + ScopedWorkspaceContextFactory, + DatabaseEventTriggerListener, + WorkflowEventTriggerJob, + ], + exports: [WorkflowTriggerWorkspaceService], }) export class WorkflowTriggerModule {} diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/workflow-trigger.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/workflow-trigger.workspace-service.ts index ea661862dd98..54079003ea7d 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/workflow-trigger.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/workflow-trigger.workspace-service.ts @@ -1,5 +1,8 @@ import { Injectable } from '@nestjs/common'; +import { buildCreatedByFromWorkspaceMember } from 'src/engine/core-modules/actor/utils/build-created-by-from-workspace-member.util'; +import { User } from 'src/engine/core-modules/user/user.entity'; +import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; import { WorkflowEventListenerWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-event-listener.workspace-entity'; import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity'; @@ -19,26 +22,43 @@ export class WorkflowTriggerWorkspaceService { constructor( private readonly twentyORMManager: TwentyORMManager, private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService, + private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory, private readonly workflowRunnerWorkspaceService: WorkflowRunnerWorkspaceService, ) {} - async runWorkflowVersion(workflowVersionId: string, payload: object) { + async runWorkflowVersion( + workflowVersionId: string, + payload: object, + workspaceMemberId: string, + user: User, + ) { + const workspaceId = this.scopedWorkspaceContextFactory.create().workspaceId; + + if (!workspaceId) { + throw new WorkflowTriggerException( + 'No workspace id found', + WorkflowTriggerExceptionCode.INTERNAL_ERROR, + ); + } + const workflowVersion = await this.workflowCommonWorkspaceService.getWorkflowVersion( workflowVersionId, ); - try { - return await this.workflowRunnerWorkspaceService.run({ - action: workflowVersion.trigger.nextAction, - payload, - }); - } catch (error) { + if (!workflowVersion) { throw new WorkflowTriggerException( - `Error running workflow version ${error}`, - WorkflowTriggerExceptionCode.INTERNAL_ERROR, + 'No workflow version found', + WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION, ); } + + return await this.workflowRunnerWorkspaceService.run( + workspaceId, + workflowVersionId, + payload, + buildCreatedByFromWorkspaceMember(workspaceMemberId, user), + ); } async enableWorkflowTrigger(workflowVersionId: string) { diff --git a/packages/twenty-server/src/modules/workflow/workflow.module.ts b/packages/twenty-server/src/modules/workflow/workflow.module.ts index 97536cf9045c..5f794b972a4d 100644 --- a/packages/twenty-server/src/modules/workflow/workflow.module.ts +++ b/packages/twenty-server/src/modules/workflow/workflow.module.ts @@ -1,9 +1,8 @@ import { Module } from '@nestjs/common'; -import { WorkflowRunnerModule } from 'src/modules/workflow/workflow-runner/workflow-runner.module'; import { WorkflowTriggerModule } from 'src/modules/workflow/workflow-trigger/workflow-trigger.module'; @Module({ - imports: [WorkflowRunnerModule, WorkflowTriggerModule], + imports: [WorkflowTriggerModule], }) export class WorkflowModule {}