Skip to content

Commit

Permalink
Trigger workflow run manually (twentyhq#6696)
Browse files Browse the repository at this point in the history
Fix twentyhq#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
  • Loading branch information
thomtrp authored Aug 21, 2024
1 parent da5dfb7 commit 663acd5
Show file tree
Hide file tree
Showing 43 changed files with 452 additions and 316 deletions.
1 change: 1 addition & 0 deletions packages/twenty-server/@types/express.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ declare module 'express-serve-static-core' {
workspace?: Workspace;
workspaceId?: string;
workspaceMetadataVersion?: number;
workspaceMemberId?: string;
}
}
Original file line number Diff line number Diff line change
@@ -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 {}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Logger } from '@nestjs/common/services/logger.service';
import { InjectRepository } from '@nestjs/typeorm';

import { Repository } from 'typeorm';
Expand All @@ -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,
Expand All @@ -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')
Expand Down Expand Up @@ -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<WorkspaceMemberWorkspaceEntity>(
authContext.workspace.id,
Expand Down
Original file line number Diff line number Diff line change
@@ -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}`,
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -66,6 +67,10 @@ describe('TokenService', () => {
provide: getRepositoryToken(Workspace, 'core'),
useValue: {},
},
{
provide: TwentyORMGlobalManager,
useValue: {},
},
],
}).compile();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -55,6 +57,7 @@ export class TokenService {
@InjectRepository(Workspace, 'core')
private readonly workspaceRepository: Repository<Workspace>,
private readonly emailService: EmailService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}

async generateAccessToken(
Expand Down Expand Up @@ -91,9 +94,33 @@ export class TokenService {
);
}

const workspaceIdNonNullable = workspaceId
? workspaceId
: user.defaultWorkspace.id;

const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
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 {
Expand Down Expand Up @@ -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<string> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down Expand Up @@ -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 };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';

Expand All @@ -36,8 +37,9 @@ import { FileModule } from './file/file.module';
WorkspaceModule,
AISQLQueryModule,
PostgresCredentialsModule,
WorkflowTriggerCoreModule,
WorkflowTriggerApiModule,
WorkspaceEventEmitterModule,
ActorModule,
],
exports: [
AnalyticsModule,
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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;
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Catch, ExceptionFilter } from '@nestjs/common';

import {
InternalServerError,
UserInputError,
Expand All @@ -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;
};
}
Original file line number Diff line number Diff line change
@@ -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 {}
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
);
}
}
Original file line number Diff line number Diff line change
@@ -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;
},
);
Loading

0 comments on commit 663acd5

Please sign in to comment.