Update workspace entities to make all TEXT nullable (#16144)

Follow up on #15926

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
Co-authored-by: guillim <guigloo@msn.com>
This commit is contained in:
Charles Bochet
2025-11-28 10:50:38 +01:00
committed by GitHub
parent 9620a4b0ba
commit eb362c6d5f
63 changed files with 396 additions and 136 deletions
@@ -1,5 +1,6 @@
import { Scope } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { And, Any, ILike, In, Not, Or } from 'typeorm';
import { type ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
@@ -54,6 +55,10 @@ export class BlocklistItemDeleteMessagesJob {
acc.set(workspaceMemberId, []);
}
if (!isDefined(handle)) {
return acc;
}
acc.get(workspaceMemberId)?.push(handle);
return acc;
@@ -146,7 +146,8 @@ export class MessageChannelWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Handle`,
icon: 'IconAt',
})
handle: string;
@WorkspaceIsNullable()
handle: string | null;
@WorkspaceField({
standardId: MESSAGE_CHANNEL_STANDARD_FIELD_IDS.type,
@@ -304,7 +305,8 @@ export class MessageChannelWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Last sync cursor`,
icon: 'IconHistory',
})
syncCursor: string;
@WorkspaceIsNullable()
syncCursor: string | null;
@WorkspaceField({
standardId: MESSAGE_CHANNEL_STANDARD_FIELD_IDS.syncedAt,
@@ -1,9 +1,9 @@
import { registerEnumType } from '@nestjs/graphql';
import { msg } from '@lingui/core/macro';
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
import { FieldMetadataType, RelationOnDeleteAction } from 'twenty-shared/types';
import { Relation } from 'typeorm';
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
@@ -47,7 +47,8 @@ export class MessageFolderWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Folder name`,
icon: 'IconFolder',
})
name: string;
@WorkspaceIsNullable()
name: string | null;
@WorkspaceRelation({
standardId: MESSAGE_FOLDER_STANDARD_FIELD_IDS.messageChannel,
@@ -68,7 +69,8 @@ export class MessageFolderWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Sync Cursor`,
icon: 'IconHash',
})
syncCursor: string;
@WorkspaceIsNullable()
syncCursor: string | null;
@WorkspaceField({
standardId: MESSAGE_FOLDER_STANDARD_FIELD_IDS.isSentFolder,
@@ -1,6 +1,6 @@
import { msg } from '@lingui/core/macro';
import { FieldMetadataType, RelationOnDeleteAction } from 'twenty-shared/types';
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
import { FieldMetadataType, RelationOnDeleteAction } from 'twenty-shared/types';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
@@ -55,7 +55,8 @@ export class MessageParticipantWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Handle`,
icon: 'IconAt',
})
handle: string;
@WorkspaceIsNullable()
handle: string | null;
@WorkspaceField({
standardId: MESSAGE_PARTICIPANT_STANDARD_FIELD_IDS.displayName,
@@ -64,7 +65,8 @@ export class MessageParticipantWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Display Name`,
icon: 'IconUser',
})
displayName: string;
@WorkspaceIsNullable()
displayName: string | null;
@WorkspaceRelation({
standardId: MESSAGE_PARTICIPANT_STANDARD_FIELD_IDS.message,
@@ -1,6 +1,6 @@
import { msg } from '@lingui/core/macro';
import { FieldMetadataType, RelationOnDeleteAction } from 'twenty-shared/types';
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
import { FieldMetadataType, RelationOnDeleteAction } from 'twenty-shared/types';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
@@ -39,7 +39,8 @@ export class MessageWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Message id from the message header`,
icon: 'IconHash',
})
headerMessageId: string;
@WorkspaceIsNullable()
headerMessageId: string | null;
@WorkspaceField({
standardId: MESSAGE_STANDARD_FIELD_IDS.subject,
@@ -48,7 +49,8 @@ export class MessageWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Subject`,
icon: 'IconMessage',
})
subject: string;
@WorkspaceIsNullable()
subject: string | null;
@WorkspaceField({
standardId: MESSAGE_STANDARD_FIELD_IDS.text,
@@ -57,7 +59,8 @@ export class MessageWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Text`,
icon: 'IconMessage',
})
text: string;
@WorkspaceIsNullable()
text: string | null;
@WorkspaceField({
standardId: MESSAGE_STANDARD_FIELD_IDS.receivedAt,
@@ -218,7 +218,7 @@ export class SyncMessageFoldersService {
if (isDefined(existingFolder.externalId)) {
existingFolderMap.set(existingFolder.externalId, existingFolder);
}
existingFolderMap.set(existingFolder.name, existingFolder);
existingFolderMap.set(existingFolder.name ?? '', existingFolder);
}
return existingFolderMap;
@@ -250,6 +250,6 @@ export class SyncMessageFoldersService {
return MessageFolderName.SENT_ITEMS;
}
return folder.name;
return folder.name ?? '';
}
}
@@ -43,4 +43,6 @@ export enum MessageImportDriverExceptionCode {
SYNC_CURSOR_ERROR = 'SYNC_CURSOR_ERROR',
PROVIDER_NOT_SUPPORTED = 'PROVIDER_NOT_SUPPORTED',
CLIENT_NOT_AVAILABLE = 'CLIENT_NOT_AVAILABLE',
ACCESS_TOKEN_MISSING = 'ACCESS_TOKEN_MISSING',
CHANNEL_MISCONFIGURED = 'CHANNEL_MISCONFIGURED',
}
@@ -5,6 +5,10 @@ import { type gmail_v1 as gmailV1 } from 'googleapis';
import { isDefined } from 'twenty-shared/utils';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import {
MessageImportDriverException,
MessageImportDriverExceptionCode,
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
import { GmailFetchByBatchService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-fetch-by-batch.service';
import { GmailMessagesImportErrorHandler } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-messages-import-error-handler.service';
import { parseAndFormatGmailMessage } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-and-format-gmail-message.util';
@@ -24,6 +28,12 @@ export class GmailGetMessagesService {
'accessToken' | 'id' | 'handle' | 'handleAliases'
>,
): Promise<MessageWithParticipants[]> {
if (!isDefined(connectedAccount.accessToken)) {
throw new MessageImportDriverException(
'Access token is required',
MessageImportDriverExceptionCode.ACCESS_TOKEN_MISSING,
);
}
const { messageIdsByBatch, batchResponses } =
await this.fetchByBatchesService.fetchAllByBatches(
messageIds,
@@ -1,12 +1,15 @@
import { Injectable, Logger } from '@nestjs/common';
import { isNonEmptyString } from '@sniptt/guards';
import { ImapFlow } from 'imapflow';
import { ConnectedAccountProvider } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { CustomError, isDefined } from 'twenty-shared/utils';
import { type ImapSmtpCaldavParams } from 'src/engine/core-modules/imap-smtp-caldav-connection/types/imap-smtp-caldav-connection.type';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import {
MessageImportDriverException,
MessageImportDriverExceptionCode,
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
type ConnectedAccountIdentifier = Pick<
ConnectedAccountWorkspaceEntity,
@@ -83,13 +86,26 @@ export class ImapClientProvider {
let client: ImapFlow | null = null;
let timeoutId: NodeJS.Timeout | null = null;
if (!isDefined(connectionParameters.IMAP?.username)) {
throw new MessageImportDriverException(
'Username is required',
MessageImportDriverExceptionCode.CHANNEL_MISCONFIGURED,
);
}
if (!isDefined(connectedAccount.handle)) {
throw new CustomError(
'Handle is required',
MessageImportDriverExceptionCode.CHANNEL_MISCONFIGURED,
);
}
try {
client = new ImapFlow({
host: connectionParameters.IMAP?.host || '',
port: connectionParameters.IMAP?.port || 993,
secure: connectionParameters.IMAP?.secure,
auth: {
user: isNonEmptyString(connectionParameters.IMAP?.username)
user: isDefined(connectionParameters.IMAP?.username)
? connectionParameters.IMAP?.username
: connectedAccount.handle,
pass: connectionParameters.IMAP?.password || '',
@@ -1,11 +1,16 @@
import { Injectable, Logger } from '@nestjs/common';
import { type ImapFlow } from 'imapflow';
import { isDefined } from 'twenty-shared/utils';
import { type MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import {
MessageImportDriverException,
MessageImportDriverExceptionCode,
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
import { ImapClientProvider } from 'src/modules/messaging/message-import-manager/drivers/imap/providers/imap-client.provider';
import { ImapMessageListFetchErrorHandler } from 'src/modules/messaging/message-import-manager/drivers/imap/services/imap-message-list-fetch-error-handler.service';
import { ImapIncrementalSyncService } from 'src/modules/messaging/message-import-manager/drivers/imap/services/imap-incremental-sync.service';
import { ImapMessageListFetchErrorHandler } from 'src/modules/messaging/message-import-manager/drivers/imap/services/imap-message-list-fetch-error-handler.service';
import { createSyncCursor } from 'src/modules/messaging/message-import-manager/drivers/imap/utils/create-sync-cursor.util';
import { extractMailboxState } from 'src/modules/messaging/message-import-manager/drivers/imap/utils/extract-mailbox-state.util';
import {
@@ -67,7 +72,7 @@ export class ImapGetMessageListService {
result.push({
messageExternalIds: [],
nextSyncCursor: folder.syncCursor || '',
previousSyncCursor: folder.syncCursor,
previousSyncCursor: folder.syncCursor || '',
messageExternalIdsToDelete: [],
folderId: folder.id,
});
@@ -86,7 +91,7 @@ export class ImapGetMessageListService {
return messageFolders.map((folder) => ({
messageExternalIds: [],
nextSyncCursor: folder.syncCursor || '',
previousSyncCursor: folder.syncCursor,
previousSyncCursor: folder.syncCursor || '',
messageExternalIdsToDelete: [],
folderId: folder.id,
}));
@@ -102,6 +107,13 @@ export class ImapGetMessageListService {
folder: string,
messageFolder: Pick<MessageFolderWorkspaceEntity, 'syncCursor'>,
): Promise<GetOneMessageListResponse> {
if (!isDefined(messageFolder.syncCursor)) {
throw new MessageImportDriverException(
'Message folder sync cursor is required',
MessageImportDriverExceptionCode.SYNC_CURSOR_ERROR,
);
}
const { messages, messageExternalUidsToDelete, syncCursor } =
await this.getMessagesFromFolder(
client,
@@ -6,6 +6,7 @@ import {
type PageIteratorCallback,
} from '@microsoft/microsoft-graph-client';
import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
@@ -115,6 +116,13 @@ export class MicrosoftGetMessageListService {
this.microsoftMessageListFetchErrorHandler.handleError(error);
});
if (!isDefined(messageFolder.syncCursor)) {
throw new MessageImportDriverException(
'Message folder sync cursor is required',
MessageImportDriverExceptionCode.SYNC_CURSOR_ERROR,
);
}
return {
messageExternalIds,
messageExternalIdsToDelete,
@@ -1,6 +1,9 @@
import { Injectable } from '@nestjs/common';
import { createTransport, type Transporter } from 'nodemailer';
import { isDefined } from 'twenty-shared/utils';
import type SMTPConnection from 'nodemailer/lib/smtp-connection';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
@@ -14,21 +17,23 @@ export class SmtpClientProvider {
): Promise<Transporter> {
const smtpParams = connectedAccount.connectionParameters?.SMTP;
if (!smtpParams) {
if (!isDefined(smtpParams)) {
throw new Error('SMTP settings not configured for this account');
}
const transporter = createTransport({
const options: SMTPConnection.Options = {
host: smtpParams.host,
port: smtpParams.port,
auth: {
user: smtpParams.username ?? connectedAccount.handle,
user: smtpParams.username ?? connectedAccount.handle ?? '',
pass: smtpParams.password,
},
tls: {
rejectUnauthorized: false,
},
});
};
const transporter = createTransport(options);
return transporter;
}
@@ -87,6 +87,8 @@ export class MessageImportExceptionHandlerService {
workspaceId,
);
break;
case MessageImportDriverExceptionCode.CHANNEL_MISCONFIGURED:
case MessageImportDriverExceptionCode.ACCESS_TOKEN_MISSING:
case MessageImportDriverExceptionCode.UNKNOWN:
case MessageImportDriverExceptionCode.UNKNOWN_NETWORK_ERROR:
default:
@@ -1,5 +1,7 @@
import { Injectable, Logger } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
@@ -14,6 +16,10 @@ import {
MessageChannelSyncStage,
type MessageChannelWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import {
MessageImportDriverException,
MessageImportDriverExceptionCode,
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
import { MESSAGING_GMAIL_USERS_MESSAGES_GET_BATCH_SIZE } from 'src/modules/messaging/message-import-manager/drivers/gmail/constants/messaging-gmail-users-messages-get-batch-size.constant';
import { MessagingAccountAuthenticationService } from 'src/modules/messaging/message-import-manager/services/messaging-account-authentication.service';
import { MessagingGetMessagesService } from 'src/modules/messaging/message-import-manager/services/messaging-get-messages.service';
@@ -114,11 +120,27 @@ export class MessagingMessagesImportService {
workspaceId,
);
if (!isDefined(messageChannel.handle)) {
throw new MessageImportDriverException(
'Message channel handle is required',
MessageImportDriverExceptionCode.CHANNEL_MISCONFIGURED,
);
}
if (!isDefined(connectedAccountWithFreshTokens.handleAliases)) {
throw new MessageImportDriverException(
'Message channel handle is required',
MessageImportDriverExceptionCode.CHANNEL_MISCONFIGURED,
);
}
const messagesToSave = filterEmails(
messageChannel.handle,
[...connectedAccountWithFreshTokens.handleAliases.split(',')],
allMessages,
blocklist.map((blocklistItem) => blocklistItem.handle),
blocklist
.map((blocklistItem) => blocklistItem.handle)
.filter(isDefined),
messageChannel.excludeGroupEmails,
);
@@ -8,6 +8,10 @@ import { z } from 'zod';
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import {
MessageImportDriverException,
MessageImportDriverExceptionCode,
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
import { ImapClientProvider } from 'src/modules/messaging/message-import-manager/drivers/imap/providers/imap-client.provider';
import { SmtpClientProvider } from 'src/modules/messaging/message-import-manager/drivers/smtp/providers/smtp-client.provider';
import { mimeEncode } from 'src/modules/messaging/message-import-manager/utils/mime-encode.util';
@@ -140,6 +144,13 @@ export class MessagingSendMessageService {
const smtpClient =
await this.smtpClientProvider.getSmtpClient(connectedAccount);
if (!isDefined(handle)) {
throw new MessageImportDriverException(
'Handle is required',
MessageImportDriverExceptionCode.CHANNEL_MISCONFIGURED,
);
}
const mail = new MailComposer({
from: handle,
to: sendMessageInput.to,
@@ -178,7 +189,7 @@ export class MessagingSendMessageService {
(messageFolder) => messageFolder.isSentFolder,
);
if (isDefined(sentFolder)) {
if (isDefined(sentFolder) && isDefined(sentFolder.name)) {
await imapClient.append(sentFolder.name, messageBuffer);
}
@@ -1,3 +1,5 @@
import { isDefined } from 'twenty-shared/utils';
import { type MessageWithParticipants } from 'src/modules/messaging/message-import-manager/types/message';
import { getDomainNameByEmail } from 'src/utils/get-domain-name-by-email';
@@ -14,9 +16,10 @@ export const filterOutInternals = (
try {
const isAllHandlesFromSameDomain = message.participants
.filter((participant) => !!participant.handle)
.filter((participant) => isDefined(participant.handle))
.every(
(participant) =>
isDefined(participant.handle) &&
getDomainNameByEmail(participant.handle) === primaryHandleDomain,
);