Trigger workflow run manually (#6696)
Fix https://github.com/twentyhq/twenty/issues/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
This commit is contained in:
@ -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 {}
|
||||
@ -0,0 +1,114 @@
|
||||
import { Logger } from '@nestjs/common/services/logger.service';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
|
||||
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,
|
||||
FieldActorSource,
|
||||
} from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
type CustomWorkspaceItem = Omit<
|
||||
CustomWorkspaceEntity,
|
||||
'createdAt' | 'updatedAt'
|
||||
> & {
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
@WorkspaceQueryHook(`*.createMany`)
|
||||
export class CreatedByPreQueryHook implements WorkspaceQueryHookInstance {
|
||||
private readonly logger = new Logger(CreatedByPreQueryHook.name);
|
||||
|
||||
constructor(
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
@InjectRepository(FieldMetadataEntity, 'metadata')
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
authContext: AuthContext,
|
||||
objectName: string,
|
||||
payload: CreateManyResolverArgs<CustomWorkspaceItem>,
|
||||
): Promise<CreateManyResolverArgs<CustomWorkspaceItem>> {
|
||||
let createdBy: ActorMetadata | null = null;
|
||||
|
||||
// TODO: Once all objects have it, we can remove this check
|
||||
const createdByFieldMetadata = await this.fieldMetadataRepository.findOne({
|
||||
where: {
|
||||
object: {
|
||||
nameSingular: objectName,
|
||||
},
|
||||
name: 'createdBy',
|
||||
workspaceId: authContext.workspace.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!createdByFieldMetadata) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
// If user is logged in, we use the workspace member
|
||||
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,
|
||||
'workspaceMember',
|
||||
);
|
||||
|
||||
const workspaceMember = await workspaceMemberRepository.findOne({
|
||||
where: {
|
||||
userId: authContext.user?.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!workspaceMember) {
|
||||
throw new Error(
|
||||
`Workspace member can't be found for user ${authContext.user.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
createdBy = {
|
||||
source: FieldActorSource.MANUAL,
|
||||
workspaceMemberId: workspaceMember.id,
|
||||
name: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (authContext.apiKey) {
|
||||
createdBy = {
|
||||
source: FieldActorSource.API,
|
||||
name: authContext.apiKey.name,
|
||||
};
|
||||
}
|
||||
|
||||
for (const datum of payload.data) {
|
||||
// Front-end can fill the source field
|
||||
if (createdBy && (!datum.createdBy || !datum.createdBy.name)) {
|
||||
datum.createdBy = {
|
||||
...createdBy,
|
||||
source: datum.createdBy?.source ?? createdBy.source,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
@ -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}`,
|
||||
});
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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<Workspace>,
|
||||
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<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 {
|
||||
@ -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> {
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 {}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
};
|
||||
}
|
||||
@ -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 {}
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user