Decouple Send Email node from workflows (#13322)
- Renamed `WorkflowActionAdapter` to `ToolExecutorWorkflowAction` - Renamed `settingPermission` table to `permissionFlag` and `setting` column to `flag` - Decoupled the send email logic from workflows to tools - Add new `Tools Permission` section in FE --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
@@ -1,7 +0,0 @@
|
||||
import { ToolType } from 'src/engine/core-modules/tool/enums/tool-type.enum';
|
||||
import { HttpTool } from 'src/engine/core-modules/tool/tools/http-tool/http-tool';
|
||||
import { Tool } from 'src/engine/core-modules/tool/types/tool.type';
|
||||
|
||||
export const TOOLS: Map<ToolType, Tool> = new Map([
|
||||
[ToolType.HTTP_REQUEST, new HttpTool()],
|
||||
]);
|
||||
@@ -1,3 +1,4 @@
|
||||
export enum ToolType {
|
||||
HTTP_REQUEST = 'HTTP_REQUEST',
|
||||
SEND_EMAIL = 'SEND_EMAIL',
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { ToolType } from 'src/engine/core-modules/tool/enums/tool-type.enum';
|
||||
import { HttpTool } from 'src/engine/core-modules/tool/tools/http-tool/http-tool';
|
||||
import { SendEmailTool } from 'src/engine/core-modules/tool/tools/send-email-tool/send-email-tool';
|
||||
import { SendEmailInput } from 'src/engine/core-modules/tool/tools/send-email-tool/types/send-email-input.type';
|
||||
import { Tool } from 'src/engine/core-modules/tool/types/tool.type';
|
||||
import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
|
||||
|
||||
@Injectable()
|
||||
export class ToolRegistryService {
|
||||
private readonly toolFactories: Map<ToolType, () => Tool>;
|
||||
|
||||
constructor(private readonly sendEmailTool: SendEmailTool) {
|
||||
this.toolFactories = new Map<ToolType, () => Tool>([
|
||||
[ToolType.HTTP_REQUEST, () => new HttpTool()],
|
||||
[
|
||||
ToolType.SEND_EMAIL,
|
||||
() => ({
|
||||
description: this.sendEmailTool.description,
|
||||
parameters: this.sendEmailTool.parameters,
|
||||
execute: (params) =>
|
||||
this.sendEmailTool.execute(params as SendEmailInput),
|
||||
flag: PermissionFlagType.SEND_EMAIL_TOOL,
|
||||
}),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
getTool(toolType: ToolType): Tool {
|
||||
const factory = this.toolFactories.get(toolType);
|
||||
|
||||
if (!factory) {
|
||||
throw new Error(`Unknown tool type: ${toolType}`);
|
||||
}
|
||||
|
||||
return factory();
|
||||
}
|
||||
|
||||
getAllToolTypes(): ToolType[] {
|
||||
return Array.from(this.toolFactories.keys());
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export class SendEmailToolException extends CustomException {
|
||||
constructor(message: string, code: SendEmailToolExceptionCode) {
|
||||
super(message, code);
|
||||
}
|
||||
}
|
||||
|
||||
export enum SendEmailToolExceptionCode {
|
||||
INVALID_CONNECTED_ACCOUNT_ID = 'INVALID_CONNECTED_ACCOUNT_ID',
|
||||
CONNECTED_ACCOUNT_NOT_FOUND = 'CONNECTED_ACCOUNT_NOT_FOUND',
|
||||
INVALID_EMAIL = 'INVALID_EMAIL',
|
||||
WORKSPACE_ID_NOT_FOUND = 'WORKSPACE_ID_NOT_FOUND',
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const SendEmailInputZodSchema = z.object({
|
||||
email: z.string().email().describe('The recipient email address'),
|
||||
subject: z.string().describe('The email subject line'),
|
||||
body: z.string().describe('The email body content (HTML or plain text)'),
|
||||
connectedAccountId: z
|
||||
.string()
|
||||
.uuid()
|
||||
.describe(
|
||||
'The UUID of the connected account to send the email from. Provide this only if you have it; otherwise, leave blank.',
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const SendEmailToolParametersZodSchema = z.object({
|
||||
toolDescription: z
|
||||
.string()
|
||||
.describe(
|
||||
"A clear, human-readable status message describing the email being sent. This will be shown to the user while the tool is being called, so phrase it as a present-tense status update (e.g., 'Sending email to customer about order status'). Explain what email you are sending and to whom in natural language.",
|
||||
),
|
||||
input: SendEmailInputZodSchema,
|
||||
});
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import DOMPurify from 'dompurify';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import { isDefined, isValidUuid } from 'twenty-shared/utils';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
SendEmailToolException,
|
||||
SendEmailToolExceptionCode,
|
||||
} from 'src/engine/core-modules/tool/tools/send-email-tool/exceptions/send-email-tool.exception';
|
||||
import { SendEmailToolParametersZodSchema } from 'src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.schema';
|
||||
import { SendEmailInput } from 'src/engine/core-modules/tool/tools/send-email-tool/types/send-email-input.type';
|
||||
import { ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
|
||||
import { Tool } from 'src/engine/core-modules/tool/types/tool.type';
|
||||
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { MessagingSendMessageService } from 'src/modules/messaging/message-import-manager/services/messaging-send-message.service';
|
||||
|
||||
@Injectable()
|
||||
export class SendEmailTool implements Tool {
|
||||
private readonly logger = new Logger(SendEmailTool.name);
|
||||
|
||||
description =
|
||||
'Send an email using a connected account. Requires SEND_EMAIL_TOOL permission.';
|
||||
parameters = SendEmailToolParametersZodSchema;
|
||||
|
||||
constructor(
|
||||
private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly sendMessageService: MessagingSendMessageService,
|
||||
) {}
|
||||
|
||||
private async getConnectedAccount(
|
||||
connectedAccountId: string,
|
||||
workspaceId: string,
|
||||
) {
|
||||
if (!isValidUuid(connectedAccountId)) {
|
||||
throw new SendEmailToolException(
|
||||
`Connected Account ID is not a valid UUID`,
|
||||
SendEmailToolExceptionCode.INVALID_CONNECTED_ACCOUNT_ID,
|
||||
);
|
||||
}
|
||||
|
||||
const connectedAccountRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ConnectedAccountWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'connectedAccount',
|
||||
);
|
||||
const connectedAccount = await connectedAccountRepository.findOneBy({
|
||||
id: connectedAccountId,
|
||||
});
|
||||
|
||||
if (!isDefined(connectedAccount)) {
|
||||
throw new SendEmailToolException(
|
||||
`Connected Account '${connectedAccountId}' not found`,
|
||||
SendEmailToolExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return connectedAccount;
|
||||
}
|
||||
|
||||
private async getOrThrowFirstConnectedAccountId(
|
||||
workspaceId: string,
|
||||
): Promise<string> {
|
||||
const connectedAccountRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ConnectedAccountWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'connectedAccount',
|
||||
);
|
||||
const allAccounts = await connectedAccountRepository.find();
|
||||
|
||||
if (!allAccounts || allAccounts.length === 0) {
|
||||
throw new SendEmailToolException(
|
||||
'No connected accounts found for this workspace',
|
||||
SendEmailToolExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return allAccounts[0].id;
|
||||
}
|
||||
|
||||
async execute(parameters: SendEmailInput): Promise<ToolOutput> {
|
||||
const { workspaceId } = this.scopedWorkspaceContextFactory.create();
|
||||
|
||||
const { email, subject, body } = parameters;
|
||||
let { connectedAccountId } = parameters;
|
||||
|
||||
try {
|
||||
const emailSchema = z.string().trim().email('Invalid email');
|
||||
const emailValidation = emailSchema.safeParse(email);
|
||||
|
||||
if (!emailValidation.success) {
|
||||
throw new SendEmailToolException(
|
||||
`Email '${email}' is invalid`,
|
||||
SendEmailToolExceptionCode.INVALID_EMAIL,
|
||||
);
|
||||
}
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new SendEmailToolException(
|
||||
'Workspace ID not found',
|
||||
SendEmailToolExceptionCode.WORKSPACE_ID_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (!connectedAccountId) {
|
||||
connectedAccountId =
|
||||
await this.getOrThrowFirstConnectedAccountId(workspaceId);
|
||||
}
|
||||
|
||||
const connectedAccount = await this.getConnectedAccount(
|
||||
connectedAccountId,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const window = new JSDOM('').window;
|
||||
const purify = DOMPurify(window);
|
||||
const safeBody = purify.sanitize(body || '');
|
||||
const safeSubject = purify.sanitize(subject || '');
|
||||
|
||||
await this.sendMessageService.sendMessage(
|
||||
{
|
||||
to: email,
|
||||
subject: safeSubject,
|
||||
body: safeBody,
|
||||
},
|
||||
connectedAccount,
|
||||
);
|
||||
|
||||
this.logger.log(`Email sent successfully to ${email}`);
|
||||
|
||||
return {
|
||||
result: {
|
||||
success: true,
|
||||
message: `Email sent successfully to ${email}`,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof SendEmailToolException) {
|
||||
return {
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
|
||||
this.logger.error(`Failed to send email: ${error}`);
|
||||
|
||||
return {
|
||||
error: error instanceof Error ? error.message : 'Failed to send email',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { SendEmailInputZodSchema } from 'src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.schema';
|
||||
|
||||
export type SendEmailInput = z.infer<typeof SendEmailInputZodSchema>;
|
||||
@@ -3,9 +3,11 @@ import { ZodType } from 'zod';
|
||||
|
||||
import { ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type';
|
||||
import { ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
|
||||
import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
|
||||
|
||||
export type Tool = {
|
||||
description: string;
|
||||
parameters: JSONSchema7 | ZodType;
|
||||
execute(input: ToolInput): Promise<ToolOutput>;
|
||||
flag?: PermissionFlagType;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user