Skip to content

Commit 663acd5

Browse files
authored
Trigger workflow run manually (#6696)
Fix #6669 - create a commun function `startWorkflowRun` that both create the run object and the job for executing the workflow - use it in both the `workflowEventJob` and the `runWorkflowVersion` endpoint Bonus: - use filtering for exceptions instead of a util. It avoids doing a try catch in all endpoint
1 parent da5dfb7 commit 663acd5

File tree

43 files changed

+452
-316
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+452
-316
lines changed

packages/twenty-server/@types/express.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ declare module 'express-serve-static-core' {
99
workspace?: Workspace;
1010
workspaceId?: string;
1111
workspaceMetadataVersion?: number;
12+
workspaceMemberId?: string;
1213
}
1314
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Module } from '@nestjs/common';
2+
import { TypeOrmModule } from '@nestjs/typeorm';
3+
4+
import { CreatedByPreQueryHook } from 'src/engine/core-modules/actor/query-hooks/created-by.pre-query-hook';
5+
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
6+
7+
@Module({
8+
imports: [TypeOrmModule.forFeature([FieldMetadataEntity], 'metadata')],
9+
providers: [CreatedByPreQueryHook],
10+
exports: [CreatedByPreQueryHook],
11+
})
12+
export class ActorModule {}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Logger } from '@nestjs/common/services/logger.service';
12
import { InjectRepository } from '@nestjs/typeorm';
23

34
import { Repository } from 'typeorm';
@@ -6,6 +7,7 @@ import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-que
67
import { CreateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
78

89
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
10+
import { buildCreatedByFromWorkspaceMember } from 'src/engine/core-modules/actor/utils/build-created-by-from-workspace-member.util';
911
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
1012
import {
1113
ActorMetadata,
@@ -26,6 +28,8 @@ type CustomWorkspaceItem = Omit<
2628

2729
@WorkspaceQueryHook(`*.createMany`)
2830
export class CreatedByPreQueryHook implements WorkspaceQueryHookInstance {
31+
private readonly logger = new Logger(CreatedByPreQueryHook.name);
32+
2933
constructor(
3034
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
3135
@InjectRepository(FieldMetadataEntity, 'metadata')
@@ -55,7 +59,14 @@ export class CreatedByPreQueryHook implements WorkspaceQueryHookInstance {
5559
}
5660

5761
// If user is logged in, we use the workspace member
58-
if (authContext.user) {
62+
if (authContext.workspaceMemberId && authContext.user) {
63+
createdBy = buildCreatedByFromWorkspaceMember(
64+
authContext.workspaceMemberId,
65+
authContext.user,
66+
);
67+
// TODO: remove that code once we have the workspace member id in all tokens
68+
} else if (authContext.user) {
69+
this.logger.warn("User doesn't have a workspace member id in the token");
5970
const workspaceMemberRepository =
6071
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
6172
authContext.workspace.id,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { User } from 'src/engine/core-modules/user/user.entity';
2+
import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
3+
4+
export const buildCreatedByFromWorkspaceMember = (
5+
workspaceMemberId: string,
6+
user: User,
7+
) => ({
8+
workspaceMemberId,
9+
source: FieldActorSource.MANUAL,
10+
name: `${user.firstName} ${user.lastName}`,
11+
});

packages/twenty-server/src/engine/core-modules/auth/services/token.service.spec.ts

+5
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { User } from 'src/engine/core-modules/user/user.entity';
1616
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
1717
import { EmailService } from 'src/engine/integrations/email/email.service';
1818
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
19+
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
1920

2021
import { TokenService } from './token.service';
2122

@@ -66,6 +67,10 @@ describe('TokenService', () => {
6667
provide: getRepositoryToken(Workspace, 'core'),
6768
useValue: {},
6869
},
70+
{
71+
provide: TwentyORMGlobalManager,
72+
useValue: {},
73+
},
6974
],
7075
}).compile();
7176

packages/twenty-server/src/engine/core-modules/auth/services/token.service.ts

+30-4
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ import { User } from 'src/engine/core-modules/user/user.entity';
4141
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
4242
import { EmailService } from 'src/engine/integrations/email/email.service';
4343
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
44+
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
45+
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
4446

4547
@Injectable()
4648
export class TokenService {
@@ -55,6 +57,7 @@ export class TokenService {
5557
@InjectRepository(Workspace, 'core')
5658
private readonly workspaceRepository: Repository<Workspace>,
5759
private readonly emailService: EmailService,
60+
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
5861
) {}
5962

6063
async generateAccessToken(
@@ -91,9 +94,33 @@ export class TokenService {
9194
);
9295
}
9396

97+
const workspaceIdNonNullable = workspaceId
98+
? workspaceId
99+
: user.defaultWorkspace.id;
100+
101+
const workspaceMemberRepository =
102+
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
103+
workspaceIdNonNullable,
104+
'workspaceMember',
105+
);
106+
107+
const workspaceMember = await workspaceMemberRepository.findOne({
108+
where: {
109+
userId: user.id,
110+
},
111+
});
112+
113+
if (!workspaceMember) {
114+
throw new AuthException(
115+
'User is not a member of the workspace',
116+
AuthExceptionCode.FORBIDDEN_EXCEPTION,
117+
);
118+
}
119+
94120
const jwtPayload: JwtPayload = {
95121
sub: user.id,
96122
workspaceId: workspaceId ? workspaceId : user.defaultWorkspace.id,
123+
workspaceMemberId: workspaceMember.id,
97124
};
98125

99126
return {
@@ -247,11 +274,10 @@ export class TokenService {
247274
this.environmentService.get('ACCESS_TOKEN_SECRET'),
248275
);
249276

250-
const { user, apiKey, workspace } = await this.jwtStrategy.validate(
251-
decoded as JwtPayload,
252-
);
277+
const { user, apiKey, workspace, workspaceMemberId } =
278+
await this.jwtStrategy.validate(decoded as JwtPayload);
253279

254-
return { user, apiKey, workspace };
280+
return { user, apiKey, workspace, workspaceMemberId };
255281
}
256282

257283
async verifyLoginToken(loginToken: string): Promise<string> {

packages/twenty-server/src/engine/core-modules/auth/strategies/jwt.auth.strategy.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@ import { EnvironmentService } from 'src/engine/integrations/environment/environm
1717
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
1818
import { ApiKeyWorkspaceEntity } from 'src/modules/api-key/standard-objects/api-key.workspace-entity';
1919

20-
export type JwtPayload = { sub: string; workspaceId: string; jti?: string };
20+
export type JwtPayload = {
21+
sub: string;
22+
workspaceId: string;
23+
workspaceMemberId: string;
24+
jti?: string;
25+
};
2126

2227
@Injectable()
2328
export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
@@ -95,6 +100,9 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
95100
}
96101
}
97102

98-
return { user, apiKey, workspace };
103+
// We don't check if the user is a member of the workspace yet
104+
const workspaceMemberId = payload.workspaceMemberId;
105+
106+
return { user, apiKey, workspace, workspaceMemberId };
99107
}
100108
}

packages/twenty-server/src/engine/core-modules/auth/types/auth-context.type.ts

+1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ import { ApiKeyWorkspaceEntity } from 'src/modules/api-key/standard-objects/api-
55
export type AuthContext = {
66
user?: User | null | undefined;
77
apiKey?: ApiKeyWorkspaceEntity | null | undefined;
8+
workspaceMemberId?: string;
89
workspace: Workspace;
910
};

packages/twenty-server/src/engine/core-modules/core-engine.module.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Module } from '@nestjs/common';
22

3+
import { ActorModule } from 'src/engine/core-modules/actor/actor.module';
34
import { AISQLQueryModule } from 'src/engine/core-modules/ai-sql-query/ai-sql-query.module';
45
import { AppTokenModule } from 'src/engine/core-modules/app-token/app-token.module';
56
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
@@ -11,7 +12,7 @@ import { TimelineMessagingModule } from 'src/engine/core-modules/messaging/timel
1112
import { OpenApiModule } from 'src/engine/core-modules/open-api/open-api.module';
1213
import { PostgresCredentialsModule } from 'src/engine/core-modules/postgres-credentials/postgres-credentials.module';
1314
import { UserModule } from 'src/engine/core-modules/user/user.module';
14-
import { WorkflowTriggerCoreModule } from 'src/engine/core-modules/workflow/core-workflow-trigger.module';
15+
import { WorkflowTriggerApiModule } from 'src/engine/core-modules/workflow/workflow-trigger-api.module';
1516
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
1617
import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module';
1718

@@ -36,8 +37,9 @@ import { FileModule } from './file/file.module';
3637
WorkspaceModule,
3738
AISQLQueryModule,
3839
PostgresCredentialsModule,
39-
WorkflowTriggerCoreModule,
40+
WorkflowTriggerApiModule,
4041
WorkspaceEventEmitterModule,
42+
ActorModule,
4143
],
4244
exports: [
4345
AnalyticsModule,

packages/twenty-server/src/engine/core-modules/workflow/core-workflow-trigger.module.ts

-12
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Field, ObjectType } from '@nestjs/graphql';
2+
3+
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
4+
5+
@ObjectType('WorkflowRun')
6+
export class WorkflowRunDTO {
7+
@Field(() => UUIDScalarType)
8+
workflowRunId: string;
9+
}

packages/twenty-server/src/engine/core-modules/workflow/dtos/workflow-trigger-result.dto.ts

-14
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { Catch, ExceptionFilter } from '@nestjs/common';
2+
13
import {
24
InternalServerError,
35
UserInputError,
@@ -7,18 +9,19 @@ import {
79
WorkflowTriggerExceptionCode,
810
} from 'src/modules/workflow/workflow-trigger/workflow-trigger.exception';
911

10-
export const workflowTriggerGraphqlApiExceptionHandler = (error: Error) => {
11-
if (error instanceof WorkflowTriggerException) {
12-
switch (error.code) {
12+
@Catch(WorkflowTriggerException)
13+
export class WorkflowTriggerGraphqlApiExceptionFilter
14+
implements ExceptionFilter
15+
{
16+
catch(exception: WorkflowTriggerException) {
17+
switch (exception.code) {
1318
case WorkflowTriggerExceptionCode.INVALID_INPUT:
14-
throw new UserInputError(error.message);
19+
throw new UserInputError(exception.message);
1520
case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER:
1621
case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION:
1722
case WorkflowTriggerExceptionCode.INVALID_ACTION_TYPE:
1823
default:
19-
throw new InternalServerError(error.message);
24+
throw new InternalServerError(exception.message);
2025
}
2126
}
22-
23-
throw error;
24-
};
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Module } from '@nestjs/common';
2+
3+
import { WorkflowTriggerResolver } from 'src/engine/core-modules/workflow/workflow-trigger.resolver';
4+
import { WorkflowTriggerModule } from 'src/modules/workflow/workflow-trigger/workflow-trigger.module';
5+
6+
@Module({
7+
imports: [WorkflowTriggerModule],
8+
providers: [WorkflowTriggerResolver],
9+
})
10+
export class WorkflowTriggerApiModule {}
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
1-
import { UseGuards } from '@nestjs/common';
1+
import { UseFilters, UseGuards } from '@nestjs/common';
22
import { Args, Mutation, Resolver } from '@nestjs/graphql';
33

4+
import { User } from 'src/engine/core-modules/user/user.entity';
45
import { RunWorkflowVersionInput } from 'src/engine/core-modules/workflow/dtos/run-workflow-version-input.dto';
5-
import { WorkflowTriggerResultDTO } from 'src/engine/core-modules/workflow/dtos/workflow-trigger-result.dto';
6-
import { workflowTriggerGraphqlApiExceptionHandler } from 'src/engine/core-modules/workflow/utils/workflow-trigger-graphql-api-exception-handler.util';
6+
import { WorkflowRunDTO } from 'src/engine/core-modules/workflow/dtos/workflow-run.dto';
7+
import { WorkflowTriggerGraphqlApiExceptionFilter } from 'src/engine/core-modules/workflow/filters/workflow-trigger-graphql-api-exception.filter';
8+
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
9+
import { AuthWorkspaceMemberId } from 'src/engine/decorators/auth/auth-workspace-member-id.decorator';
710
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
811
import { WorkflowTriggerWorkspaceService } from 'src/modules/workflow/workflow-trigger/workflow-trigger.workspace-service';
912

10-
@UseGuards(JwtAuthGuard)
1113
@Resolver()
14+
@UseGuards(JwtAuthGuard)
15+
@UseFilters(WorkflowTriggerGraphqlApiExceptionFilter)
1216
export class WorkflowTriggerResolver {
1317
constructor(
1418
private readonly workflowTriggerWorkspaceService: WorkflowTriggerWorkspaceService,
@@ -18,28 +22,22 @@ export class WorkflowTriggerResolver {
1822
async enableWorkflowTrigger(
1923
@Args('workflowVersionId') workflowVersionId: string,
2024
) {
21-
try {
22-
return await this.workflowTriggerWorkspaceService.enableWorkflowTrigger(
23-
workflowVersionId,
24-
);
25-
} catch (error) {
26-
workflowTriggerGraphqlApiExceptionHandler(error);
27-
}
25+
return await this.workflowTriggerWorkspaceService.enableWorkflowTrigger(
26+
workflowVersionId,
27+
);
2828
}
2929

30-
@Mutation(() => WorkflowTriggerResultDTO)
30+
@Mutation(() => WorkflowRunDTO)
3131
async runWorkflowVersion(
32+
@AuthWorkspaceMemberId() workspaceMemberId: string,
33+
@AuthUser() user: User,
3234
@Args('input') { workflowVersionId, payload }: RunWorkflowVersionInput,
3335
) {
34-
try {
35-
return {
36-
result: await this.workflowTriggerWorkspaceService.runWorkflowVersion(
37-
workflowVersionId,
38-
payload ?? {},
39-
),
40-
};
41-
} catch (error) {
42-
workflowTriggerGraphqlApiExceptionHandler(error);
43-
}
36+
return await this.workflowTriggerWorkspaceService.runWorkflowVersion(
37+
workflowVersionId,
38+
payload ?? {},
39+
workspaceMemberId,
40+
user,
41+
);
4442
}
4543
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { ExecutionContext, createParamDecorator } from '@nestjs/common';
2+
3+
import { getRequest } from 'src/utils/extract-request';
4+
5+
export const AuthWorkspaceMemberId = createParamDecorator(
6+
(data: unknown, ctx: ExecutionContext) => {
7+
const request = getRequest(ctx);
8+
9+
return request.workspaceMemberId;
10+
},
11+
);

0 commit comments

Comments
 (0)