Compare commits

...

2 Commits

Author SHA1 Message Date
claude[bot] 3881cdd675 refactor(emailing-domain): convert TypeORM migration to instance command
Twenty replaced raw TypeORM migrations with the instance command system
(see packages/twenty-server/docs/UPGRADE_COMMANDS.md). The previous
1780754108000-add-log-emailing-domain-driver.ts under
src/database/typeorm/core/migrations/common/ would never run via the
upgrade pipeline. Move it to a 2.11 FastInstanceCommand registered in
INSTANCE_COMMANDS, with the same ALTER TYPE ... ADD VALUE LOG body.

Co-authored-by: Félix Malfait <FelixMalfait@users.noreply.github.com>
2026-06-07 12:28:10 +00:00
Félix Malfait 892022de04 feat(emailing-domain): add LOG driver for local development
Adds a LOG driver to the emailing-domain feature so the verify → send
flow can run without AWS credentials. The driver is selected via a new
EMAILING_DOMAIN_DRIVER config variable (defaults to AWS_SES, preserving
production behavior).

Also dev-seeds a pre-verified domain per workspace so the feature is
usable out of the box. Set EMAILING_DOMAIN_DRIVER=LOG to exercise sends:
each send is logged and returns a synthetic messageId instead of calling
SES.
2026-06-06 16:11:33 +02:00
12 changed files with 199 additions and 12 deletions
@@ -1454,6 +1454,7 @@ type EmailingDomain {
enum EmailingDomainDriver {
AWS_SES
LOG
}
enum EmailingDomainStatus {
@@ -1111,7 +1111,7 @@ export interface EmailingDomain {
__typename: 'EmailingDomain'
}
export type EmailingDomainDriver = 'AWS_SES'
export type EmailingDomainDriver = 'AWS_SES' | 'LOG'
export type EmailingDomainStatus = 'PENDING' | 'VERIFIED' | 'FAILED' | 'TEMPORARY_FAILURE'
@@ -8733,7 +8733,8 @@ export const enumPageLayoutType = {
}
export const enumEmailingDomainDriver = {
AWS_SES: 'AWS_SES' as const
AWS_SES: 'AWS_SES' as const,
LOG: 'LOG' as const
}
export const enumEmailingDomainStatus = {
@@ -1473,7 +1473,8 @@ export type EmailingDomain = {
};
export enum EmailingDomainDriver {
AWS_SES = 'AWS_SES'
AWS_SES = 'AWS_SES',
LOG = 'LOG'
}
export enum EmailingDomainStatus {
@@ -0,0 +1,31 @@
import { type QueryRunner } from 'typeorm';
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
import { type FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
@RegisteredInstanceCommand('2.11.0', 1780754108000)
export class AddLogEmailingDomainDriverFastInstanceCommand
implements FastInstanceCommand
{
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TYPE "core"."emailingDomain_driver_enum" ADD VALUE IF NOT EXISTS 'LOG' AFTER 'AWS_SES'`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."emailingDomain" ALTER COLUMN "driver" TYPE character varying`,
);
await queryRunner.query(
`UPDATE "core"."emailingDomain" SET "driver" = 'AWS_SES' WHERE "driver" = 'LOG'`,
);
await queryRunner.query(`DROP TYPE "core"."emailingDomain_driver_enum"`);
await queryRunner.query(
`CREATE TYPE "core"."emailingDomain_driver_enum" AS ENUM('AWS_SES')`,
);
await queryRunner.query(
`ALTER TABLE "core"."emailingDomain" ALTER COLUMN "driver" TYPE "core"."emailingDomain_driver_enum" USING "driver"::"core"."emailingDomain_driver_enum"`,
);
}
}
@@ -59,6 +59,7 @@ import { EmailingDomainTenantStatusAndGlobalUniquenessFastInstanceCommand } from
import { AddLogicFunctionExecutionModeFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-9/2-9-instance-command-fast-1799000030000-add-logic-function-execution-mode';
import { EncryptNonSecretApplicationVariableSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-9/2-9-instance-command-slow-1798400000000-encrypt-non-secret-application-variable';
import { MigrateAiModelPreferencesSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-9/2-9-instance-command-slow-1799000010000-migrate-ai-model-preferences';
import { AddLogEmailingDomainDriverFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-11/2-11-instance-command-fast-1780754108000-add-log-emailing-domain-driver';
export const INSTANCE_COMMANDS = [
AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand,
@@ -120,4 +121,5 @@ export const INSTANCE_COMMANDS = [
AddLogicFunctionExecutionModeFastInstanceCommand,
MigrateAiModelPreferencesSlowInstanceCommand,
EncryptNonSecretApplicationVariableSlowInstanceCommand,
AddLogEmailingDomainDriverFastInstanceCommand,
];
@@ -8,6 +8,7 @@ import { AwsSesRegisterDomainService } from 'src/engine/core-modules/emailing-do
import { AwsSesDriver } from 'src/engine/core-modules/emailing-domain/drivers/aws-ses/services/aws-ses-driver.service';
import { AwsSesHandleErrorService } from 'src/engine/core-modules/emailing-domain/drivers/aws-ses/services/aws-ses-handle-error.service';
import { AwsSesSendEmailService } from 'src/engine/core-modules/emailing-domain/drivers/aws-ses/services/aws-ses-send-email.service';
import { LogEmailingDomainDriver } from 'src/engine/core-modules/emailing-domain/drivers/log/services/log-emailing-domain-driver.service';
import { EmailingDomainDriver } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-driver.type';
import { DriverFactoryBase } from 'src/engine/core-modules/twenty-config/dynamic-factory.base';
import { ConfigVariablesGroup } from 'src/engine/core-modules/twenty-config/enums/config-variables-group.enum';
@@ -23,26 +24,31 @@ export class EmailingDomainDriverFactory extends DriverFactoryBase<EmailingDomai
private readonly awsSesHandleErrorService: AwsSesHandleErrorService,
private readonly awsSesRegisterDomainService: AwsSesRegisterDomainService,
private readonly awsSesSendEmailService: AwsSesSendEmailService,
private readonly logEmailingDomainDriver: LogEmailingDomainDriver,
) {
super(twentyConfigService, configGroupHashService);
}
protected buildConfigKey(): string {
const driver = EmailingDomainDriver.AWS_SES;
const driver = this.twentyConfigService.get('EMAILING_DOMAIN_DRIVER');
if (driver === EmailingDomainDriver.AWS_SES) {
const awsConfigHash = this.configGroupHashService.computeHash(
ConfigVariablesGroup.AWS_SES_SETTINGS,
);
switch (driver) {
case EmailingDomainDriver.AWS_SES: {
const awsConfigHash = this.configGroupHashService.computeHash(
ConfigVariablesGroup.AWS_SES_SETTINGS,
);
return `aws-ses|${awsConfigHash}`;
return `aws-ses|${awsConfigHash}`;
}
case EmailingDomainDriver.LOG:
return 'log';
default:
throw new Error(`Unsupported emailing domain driver: ${driver}`);
}
throw new Error(`Unsupported emailing domain driver: ${driver}`);
}
protected createDriver(): EmailingDomainDriverInterface {
const driver = EmailingDomainDriver.AWS_SES;
const driver = this.twentyConfigService.get('EMAILING_DOMAIN_DRIVER');
switch (driver) {
case EmailingDomainDriver.AWS_SES: {
@@ -76,6 +82,9 @@ export class EmailingDomainDriverFactory extends DriverFactoryBase<EmailingDomai
);
}
case EmailingDomainDriver.LOG:
return this.logEmailingDomainDriver;
default:
throw new Error(`Invalid emailing domain driver: ${driver}`);
}
@@ -0,0 +1,71 @@
import { Injectable, Logger } from '@nestjs/common';
import { v4 } from 'uuid';
import {
type EmailingDomainDriverInterface,
type EmailingDomainResourceInput,
type EmailingDomainVerificationResult,
} from 'src/engine/core-modules/emailing-domain/drivers/interfaces/emailing-domain-driver.interface';
import { EmailingDomainStatus } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-status.type';
import {
type EmailingDomainSendEmailInput,
type EmailingDomainSendEmailResult,
} from 'src/engine/core-modules/emailing-domain/drivers/types/send-email';
@Injectable()
export class LogEmailingDomainDriver implements EmailingDomainDriverInterface {
private readonly logger = new Logger(LogEmailingDomainDriver.name);
async provisionWorkspace(workspaceId: string): Promise<void> {
this.logger.log(`[log-driver] provisionWorkspace(${workspaceId})`);
}
async deprovisionWorkspace(workspaceId: string): Promise<void> {
this.logger.log(`[log-driver] deprovisionWorkspace(${workspaceId})`);
}
async verifyDomain(
input: EmailingDomainResourceInput,
): Promise<EmailingDomainVerificationResult> {
this.logger.log(
`[log-driver] verifyDomain(${input.domain}) → VERIFIED (instant)`,
);
return {
status: EmailingDomainStatus.VERIFIED,
verificationRecords: [],
};
}
async getDomainStatus(
input: EmailingDomainResourceInput,
): Promise<EmailingDomainVerificationResult> {
this.logger.log(`[log-driver] getDomainStatus(${input.domain}) → VERIFIED`);
return {
status: EmailingDomainStatus.VERIFIED,
verificationRecords: [],
};
}
async registerDomain(input: EmailingDomainResourceInput): Promise<void> {
this.logger.log(`[log-driver] registerDomain(${input.domain})`);
}
async cleanupDomain(input: EmailingDomainResourceInput): Promise<void> {
this.logger.log(`[log-driver] cleanupDomain(${input.domain})`);
}
async sendEmail(
input: EmailingDomainSendEmailInput,
): Promise<EmailingDomainSendEmailResult> {
const messageId = `log-${v4()}`;
this.logger.log(
`[log-driver] sendEmail from=${input.from} to=${input.to.join(',')} subject="${input.subject}" → fake messageId=${messageId}`,
);
return { messageId };
}
}
@@ -1,3 +1,4 @@
export enum EmailingDomainDriver {
AWS_SES = 'AWS_SES',
LOG = 'LOG',
}
@@ -8,6 +8,7 @@ import { AwsSesRegisterDomainService } from 'src/engine/core-modules/emailing-do
import { AwsSesHandleErrorService } from 'src/engine/core-modules/emailing-domain/drivers/aws-ses/services/aws-ses-handle-error.service';
import { AwsSesSendEmailService } from 'src/engine/core-modules/emailing-domain/drivers/aws-ses/services/aws-ses-send-email.service';
import { EmailingDomainDriverFactory } from 'src/engine/core-modules/emailing-domain/drivers/emailing-domain-driver.factory';
import { LogEmailingDomainDriver } from 'src/engine/core-modules/emailing-domain/drivers/log/services/log-emailing-domain-driver.service';
import { EmailingDomainEntity } from 'src/engine/core-modules/emailing-domain/emailing-domain.entity';
import { EmailingDomainResolver } from 'src/engine/core-modules/emailing-domain/emailing-domain.resolver';
import { EmailingDomainWorkspaceCleanupJob } from 'src/engine/core-modules/emailing-domain/jobs/emailing-domain-workspace-cleanup.job';
@@ -34,6 +35,7 @@ import { provideWorkspaceScopedRepository } from 'src/engine/twenty-orm/workspac
AwsSesHandleErrorService,
AwsSesRegisterDomainService,
AwsSesSendEmailService,
LogEmailingDomainDriver,
provideWorkspaceScopedRepository(EmailingDomainEntity),
],
})
@@ -20,6 +20,7 @@ import { SupportDriver } from 'src/engine/core-modules/twenty-config/interfaces/
import { CaptchaDriverType } from 'src/engine/core-modules/captcha/interfaces';
import { CodeInterpreterDriverType } from 'src/engine/core-modules/code-interpreter/code-interpreter.interface';
import { EmailDriver } from 'src/engine/core-modules/email/enums/email-driver.enum';
import { EmailingDomainDriver } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-driver.type';
import { ExceptionHandlerDriver } from 'src/engine/core-modules/exception-handler/interfaces';
import { StorageDriverType } from 'src/engine/core-modules/file-storage/interfaces';
import { LoggerDriverType } from 'src/engine/core-modules/logger/interfaces';
@@ -1697,6 +1698,16 @@ export class ConfigVariables {
@IsOptional()
MINTLIFY_SUBDOMAIN: string;
@ConfigVariablesMetadata({
group: ConfigVariablesGroup.AWS_SES_SETTINGS,
description:
'Driver used for the emailing domain feature — AWS_SES for production, LOG for local development (no AWS credentials needed)',
type: ConfigVariableType.ENUM,
options: Object.values(EmailingDomainDriver),
})
@CastToUpperSnakeCase()
EMAILING_DOMAIN_DRIVER: EmailingDomainDriver = EmailingDomainDriver.AWS_SES;
@ConfigVariablesMetadata({
group: ConfigVariablesGroup.AWS_SES_SETTINGS,
description: 'AWS region',
@@ -0,0 +1,49 @@
import { type QueryRunner } from 'typeorm';
import { EmailingDomainDriver } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-driver.type';
import { EmailingDomainStatus } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-status.type';
import { EmailingDomainTenantStatus } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-tenant-status.type';
const tableName = 'emailingDomain';
const DEV_EMAILING_DOMAIN = 'dev.twenty.local';
type SeedEmailingDomainsArgs = {
queryRunner: QueryRunner;
schemaName: string;
workspaceId: string;
};
export const seedEmailingDomains = async ({
queryRunner,
schemaName,
workspaceId,
}: SeedEmailingDomainsArgs) => {
const domain = `${workspaceId.slice(0, 8)}.${DEV_EMAILING_DOMAIN}`;
await queryRunner.manager
.createQueryBuilder()
.insert()
.into(`${schemaName}.${tableName}`, [
'workspaceId',
'domain',
'driver',
'status',
'verificationRecords',
'verifiedAt',
'tenantStatus',
])
.orIgnore()
.values([
{
workspaceId,
domain,
driver: EmailingDomainDriver.LOG,
status: EmailingDomainStatus.VERIFIED,
verificationRecords: JSON.stringify([]),
verifiedAt: new Date(),
tenantStatus: EmailingDomainTenantStatus.ACTIVE,
},
])
.execute();
};
@@ -6,6 +6,7 @@ import { v4 } from 'uuid';
import { ApplicationRegistrationService } from 'src/engine/core-modules/application/application-registration/application-registration.service';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { EmailingDomainDriver } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-driver.type';
import { SdkClientGenerationService } from 'src/engine/core-modules/sdk-client/sdk-client-generation.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { UpgradeMigrationService } from 'src/engine/core-modules/upgrade/services/upgrade-migration.service';
@@ -27,6 +28,7 @@ import {
import { DevSeederPermissionsService } from 'src/engine/workspace-manager/dev-seeder/core/services/dev-seeder-permissions.service';
import { seedAgents } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-agents.util';
import { seedApiKeys } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-api-keys.util';
import { seedEmailingDomains } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-emailing-domains.util';
import { seedFeatureFlags } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util';
import { seedMetadataEntities } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-metadata-entities.util';
import { seedPageLayouts } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layouts.util';
@@ -319,6 +321,12 @@ export class DevSeederService {
await seedAgents({ queryRunner, schemaName, workspaceId });
await seedApiKeys({ queryRunner, schemaName, workspaceId });
if (
this.twentyConfigService.get('EMAILING_DOMAIN_DRIVER') ===
EmailingDomainDriver.LOG
) {
await seedEmailingDomains({ queryRunner, schemaName, workspaceId });
}
await seedFeatureFlags({ queryRunner, schemaName, workspaceId });
if (seedBilling) {