Throw proper error on duplicate emailing domain (#22790)

Adding an emailing domain that already exists blew up with a raw
QueryFailedError and the client just saw a generic "An error occurred".
The unique index on domain is global, so the workspace-scoped existence
check never caught rows owned by another workspace.

Now the check is unscoped and throws an EmailingDomainException mapped
to CONFLICT with a proper user-facing message, in both the
createEmailingDomain mutation and the email group channel flow. Also
dropped the hardcoded catch-all snackbar on the new channel page so
server messages actually reach the user.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22790?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
neo773
2026-07-10 23:58:34 +05:30
committed by GitHub
parent cb95410a51
commit b0dc637dbd
8 changed files with 113 additions and 26 deletions
@@ -8,6 +8,7 @@ import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorato
import { CreateEmailingDomainInput } from 'src/engine/core-modules/emailing-domain/dtos/create-emailing-domain.input';
import { EmailingDomainDTO } from 'src/engine/core-modules/emailing-domain/dtos/emailing-domain.dto';
import { EmailGroupAccessGraphqlApiExceptionFilter } from 'src/engine/core-modules/emailing-domain/filters/email-group-access-graphql-api-exception.filter';
import { EmailingDomainGraphqlApiExceptionFilter } from 'src/engine/core-modules/emailing-domain/filters/emailing-domain-graphql-api-exception.filter';
import { EmailGroupAccessService } from 'src/engine/core-modules/emailing-domain/services/email-group-access.service';
import { EmailingDomainService } from 'src/engine/core-modules/emailing-domain/services/emailing-domain.service';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
@@ -25,7 +26,10 @@ import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
FeatureFlagGuard,
SettingsPermissionGuard(PermissionFlagType.WORKSPACE),
)
@UseFilters(EmailGroupAccessGraphqlApiExceptionFilter)
@UseFilters(
EmailGroupAccessGraphqlApiExceptionFilter,
EmailingDomainGraphqlApiExceptionFilter,
)
@UsePipes(ResolverValidationPipe)
@MetadataResolver(() => EmailingDomainDTO)
export class EmailingDomainResolver {
@@ -0,0 +1,34 @@
import { type MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { assertUnreachable } from 'twenty-shared/utils';
import { CustomException } from 'src/utils/custom-exception';
export enum EmailingDomainExceptionCode {
EMAILING_DOMAIN_ALREADY_REGISTERED = 'EMAILING_DOMAIN_ALREADY_REGISTERED',
}
const getEmailingDomainExceptionUserFriendlyMessage = (
code: EmailingDomainExceptionCode,
) => {
switch (code) {
case EmailingDomainExceptionCode.EMAILING_DOMAIN_ALREADY_REGISTERED:
return msg`This domain is already registered.`;
default:
assertUnreachable(code);
}
};
export class EmailingDomainException extends CustomException<EmailingDomainExceptionCode> {
constructor(
message: string,
code: EmailingDomainExceptionCode,
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
) {
super(message, code, {
userFriendlyMessage:
userFriendlyMessage ??
getEmailingDomainExceptionUserFriendlyMessage(code),
});
}
}
@@ -0,0 +1,22 @@
import { Catch, type ExceptionFilter } from '@nestjs/common';
import { assertUnreachable } from 'twenty-shared/utils';
import {
EmailingDomainException,
EmailingDomainExceptionCode,
} from 'src/engine/core-modules/emailing-domain/exceptions/emailing-domain.exception';
import { ConflictError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
@Catch(EmailingDomainException)
export class EmailingDomainGraphqlApiExceptionFilter implements ExceptionFilter {
catch(exception: EmailingDomainException) {
switch (exception.code) {
case EmailingDomainExceptionCode.EMAILING_DOMAIN_ALREADY_REGISTERED:
throw new ConflictError(exception);
default: {
assertUnreachable(exception.code);
}
}
}
}
@@ -1,7 +1,10 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import {
EmailingDomainDriverException,
EmailingDomainDriverExceptionCode,
@@ -9,6 +12,10 @@ import {
import { EmailingDomainDriverFactory } from 'src/engine/core-modules/emailing-domain/drivers/emailing-domain-driver.factory';
import { EmailingDomainStatus } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-status.type';
import { EmailingDomainEntity } from 'src/engine/core-modules/emailing-domain/emailing-domain.entity';
import {
EmailingDomainException,
EmailingDomainExceptionCode,
} from 'src/engine/core-modules/emailing-domain/exceptions/emailing-domain.exception';
import { UnsubscribeHostnameService } from 'src/engine/core-modules/emailing-domain/services/unsubscribe-hostname.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { InjectWorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/inject-workspace-scoped-repository.decorator';
@@ -21,6 +28,10 @@ export class EmailingDomainService {
constructor(
@InjectWorkspaceScopedRepository(EmailingDomainEntity)
private readonly emailingDomainRepository: WorkspaceScopedRepository<EmailingDomainEntity>,
// Domain is globally unique across workspaces, so existence checks need
// an unscoped repository
@InjectRepository(EmailingDomainEntity)
private readonly globalEmailingDomainRepository: Repository<EmailingDomainEntity>,
private readonly emailingDomainDriverFactory: EmailingDomainDriverFactory,
private readonly unsubscribeHostnameService: UnsubscribeHostnameService,
) {}
@@ -29,17 +40,15 @@ export class EmailingDomainService {
domain: string,
workspaceId: string,
): Promise<EmailingDomainEntity> {
const existingEmailingDomain = await this.emailingDomainRepository.findOne(
workspaceId,
{
const existingEmailingDomain =
await this.globalEmailingDomainRepository.findOne({
where: { domain },
},
);
});
if (existingEmailingDomain) {
throw new EmailingDomainDriverException(
'Emailing domain already exists for this workspace',
EmailingDomainDriverExceptionCode.CONFIGURATION_ERROR,
if (isDefined(existingEmailingDomain)) {
throw new EmailingDomainException(
'Emailing domain is already registered',
EmailingDomainExceptionCode.EMAILING_DOMAIN_ALREADY_REGISTERED,
);
}