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,6 +1,7 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
@@ -62,6 +63,10 @@ export class MigrateAttachmentTypeToFileCategoryCommand extends ActiveOrSuspende
for (const attachment of attachments) {
const { id, type } = attachment;
if (!isDefined(type)) {
throw new Error(`Attachment ${id} has no type`);
}
const fileCategory =
TYPE_TO_FILE_CATEGORY_MAPPING[type] ||
TYPE_TO_FILE_CATEGORY_MAPPING.Other;
@@ -7,10 +7,10 @@ import { msg } from '@lingui/core/macro';
import { render } from '@react-email/render';
import { SendApprovedAccessDomainValidation } from 'twenty-emails';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { ApprovedAccessDomainEntity as ApprovedAccessDomainEntity } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.entity';
import { ApprovedAccessDomainEntity } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.entity';
import {
ApprovedAccessDomainException,
ApprovedAccessDomainExceptionCode,
@@ -70,6 +70,10 @@ export class ApprovedAccessDomainService {
},
});
if (!isDefined(sender.userEmail)) {
throw new Error(`Sender ${sender.id} has an empty userEmail`);
}
const emailTemplate = SendApprovedAccessDomainValidation({
link: link.toString(),
workspace: {
@@ -112,7 +112,7 @@ export class TimelineCalendarEventService {
participant.person?.avatarUrl ||
participant.workspaceMember?.avatarUrl ||
'',
handle: participant.handle,
handle: participant.handle ?? '',
}),
);
@@ -139,15 +139,17 @@ export class TimelineCalendarEventService {
title:
visibility === CalendarChannelVisibility.METADATA
? FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED
: event.title,
: (event.title ?? ''),
description:
visibility === CalendarChannelVisibility.METADATA
? FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED
: event.description,
: (event.description ?? ''),
startsAt: event.startsAt as unknown as Date,
endsAt: event.endsAt as unknown as Date,
participants,
visibility,
location: event.location ?? '',
conferenceSolution: event.conferenceSolution ?? '',
};
});
@@ -31,7 +31,7 @@ export class FileAttachmentListener {
FileDeletionJob.name,
{
workspaceId: payload.workspaceId,
fullPath: event.properties.before.fullPath,
fullPath: event.properties.before.fullPath ?? '',
},
);
}
@@ -35,7 +35,7 @@ export class FileWorkspaceMemberListener {
this.messageQueueService.add<FileDeletionJobData>(FileDeletionJob.name, {
workspaceId: payload.workspaceId,
fullPath: event.properties.before.avatarUrl,
fullPath: event.properties.before.avatarUrl ?? '',
});
}
}
@@ -8,6 +8,7 @@ import {
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { ConnectedAccountProvider } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
@@ -59,7 +60,7 @@ export class ImapSmtpCaldavResolver {
where: { id, provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV },
});
if (!connectedAccount) {
if (!isDefined(connectedAccount) || !isDefined(connectedAccount?.handle)) {
throw new UserInputError(
`Connected mail account with ID ${id} not found`,
);
@@ -75,8 +75,8 @@ export class TimelineMessagingService {
return {
id: messageThread.id,
subject: firstMessage.subject,
lastMessageBody: lastMessage.text,
subject: firstMessage.subject ?? '',
lastMessageBody: lastMessage.text ?? '',
lastMessageReceivedAt: lastMessage.receivedAt ?? new Date(),
numberOfMessagesInThread: messageThread.messages.length,
};
@@ -1,30 +1,40 @@
import { isDefined } from 'twenty-shared/utils';
import { type TimelineThreadParticipantDTO } from 'src/engine/core-modules/messaging/dtos/timeline-thread-participant.dto';
import { type MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity';
export const formatThreadParticipant = (
threadParticipant: MessageParticipantWorkspaceEntity,
): TimelineThreadParticipantDTO => ({
personId: threadParticipant.personId,
workspaceMemberId: threadParticipant.workspaceMemberId,
firstName:
threadParticipant.person?.name?.firstName ||
threadParticipant.workspaceMember?.name.firstName ||
'',
lastName:
threadParticipant.person?.name?.lastName ||
threadParticipant.workspaceMember?.name.lastName ||
'',
displayName:
threadParticipant.person?.name?.firstName ||
threadParticipant.person?.name?.lastName ||
threadParticipant.workspaceMember?.name.firstName ||
threadParticipant.workspaceMember?.name.lastName ||
threadParticipant.displayName ||
threadParticipant.handle ||
'',
avatarUrl:
threadParticipant.person?.avatarUrl ||
threadParticipant.workspaceMember?.avatarUrl ||
'',
handle: threadParticipant.handle,
});
): TimelineThreadParticipantDTO => {
if (!isDefined(threadParticipant.handle)) {
throw new Error(
`Thread participant ${threadParticipant.id} has an empty handle`,
);
}
return {
personId: threadParticipant.personId,
workspaceMemberId: threadParticipant.workspaceMemberId,
firstName:
threadParticipant.person?.name?.firstName ||
threadParticipant.workspaceMember?.name.firstName ||
'',
lastName:
threadParticipant.person?.name?.lastName ||
threadParticipant.workspaceMember?.name.lastName ||
'',
displayName:
threadParticipant.person?.name?.firstName ||
threadParticipant.person?.name?.lastName ||
threadParticipant.workspaceMember?.name.firstName ||
threadParticipant.workspaceMember?.name.lastName ||
threadParticipant.displayName ||
threadParticipant.handle ||
'',
avatarUrl:
threadParticipant.person?.avatarUrl ||
threadParticipant.workspaceMember?.avatarUrl ||
'',
handle: threadParticipant.handle,
};
};
@@ -75,6 +75,10 @@ export class WorkspaceMemberTranspiler {
const roles = fromRoleEntitiesToRoleDtos(userWorkspaceRoles);
if (!isDefined(userEmail)) {
throw new Error(`Workspace member ${id} has no userEmail`);
}
return {
id,
name,
@@ -111,6 +115,10 @@ export class WorkspaceMemberTranspiler {
userEmail,
} = workspaceMember;
if (!isDefined(userEmail)) {
throw new Error(`Workspace member ${id} has no userEmail`);
}
const avatarUrl = userWorkspaceId
? this.generateSignedAvatarUrl({
workspaceId: userWorkspaceId,
@@ -4,6 +4,7 @@ import { Args, Mutation, Resolver } from '@nestjs/graphql';
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
import { CreateDraftFromWorkflowVersionInput } from 'src/engine/core-modules/workflow/dtos/create-draft-from-workflow-version-input.dto';
import { DuplicateWorkflowInput } from 'src/engine/core-modules/workflow/dtos/duplicate-workflow-input.dto';
import { UpdateWorkflowVersionPositionsInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-version-positions-input.dto';
import { WorkflowVersionDTO } from 'src/engine/core-modules/workflow/dtos/workflow-version.dto';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
@@ -14,7 +15,6 @@ import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
import { WorkflowVersionWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version/workflow-version.workspace-service';
import { DuplicateWorkflowInput } from 'src/engine/core-modules/workflow/dtos/duplicate-workflow-input.dto';
@Resolver()
@UsePipes(ResolverValidationPipe)
@@ -306,6 +306,13 @@ export class WorkspaceInvitationService {
: {},
});
if (!isDefined(sender.userEmail)) {
throw new WorkspaceInvitationException(
'Sender email is missing',
WorkspaceInvitationExceptionCode.EMAIL_MISSING,
);
}
const emailData = {
link: link.toString(),
workspace: {
@@ -7,8 +7,11 @@ export const transformStandardAgentDefinitionToFlatAgent = (
standardAgentDefinition: StandardAgentDefinition,
workspaceId: string,
): FlatAgent => {
const { standardRoleId: _standardRoleId, ...agentData } =
standardAgentDefinition;
const {
standardRoleId: _standardRoleId,
outputStrategy: _outputStrategy,
...agentData
} = standardAgentDefinition;
return {
...agentData,
@@ -7,9 +7,9 @@ import {
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
import { DEFAULT_LABEL_IDENTIFIER_FIELD_NAME } from 'src/engine/metadata-modules/object-metadata/constants/object-metadata.constants';
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { WorkspaceCustomEntity } from 'src/engine/twenty-orm/decorators/workspace-custom-entity.decorator';
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
@@ -44,7 +44,8 @@ export class CustomWorkspaceEntity extends BaseWorkspaceEntity {
type: FieldMetadataType.TEXT,
icon: 'IconAbc',
})
name: string;
@WorkspaceIsNullable()
name: string | null;
@WorkspaceField({
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.position,
@@ -133,6 +133,10 @@ export class CleanerWorkspaceService {
const i18n = this.i18nService.getI18nInstance(workspaceMember.locale);
const subject = i18n._(workspaceDeletionMsg);
if (!isDefined(workspaceMember.userEmail)) {
throw new Error('Workspace member email is missing');
}
this.emailService.send({
to: workspaceMember.userEmail,
from: `${this.twentyConfigService.get(
@@ -207,6 +211,10 @@ export class CleanerWorkspaceService {
const html = await render(emailTemplate, { pretty: true });
const text = await render(emailTemplate, { plainText: true });
if (!isDefined(workspaceMember.userEmail)) {
throw new Error('Workspace member email is missing');
}
this.emailService.send({
to: workspaceMember.userEmail,
from: `${this.twentyConfigService.get(
@@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common';
import { type WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
import { fromStandardRoleDefinitionToFlatRole } from 'src/engine/metadata-modules/flat-role/utils/from-standard-role-definition-to-flat-role.util';
import { type RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { type StandardRoleDefinition } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/types/standard-role-definition.interface';
@@ -13,8 +12,8 @@ export class StandardRoleFactory {
roleDefinitions: StandardRoleDefinition[],
context: WorkspaceSyncContext,
existingRoles: RoleEntity[],
): FlatRole[] {
const computedRoles: FlatRole[] = [];
): Partial<RoleEntity>[] {
const computedRoles: Partial<RoleEntity>[] = [];
for (const roleDefinition of roleDefinitions) {
const existingRole = existingRoles.find(
@@ -49,7 +49,7 @@ export class WorkspaceSyncRoleService {
const roleComparatorResults = this.workspaceRoleComparator.compare({
fromFlatRoles: existingStandardRoleEntities.map(fromRoleEntityToFlatRole),
toFlatRoles: targetStandardRoles,
toFlatRoles: targetStandardRoles.map(fromRoleEntityToFlatRole),
});
for (const roleComparatorResult of roleComparatorResults) {
@@ -1,10 +1,10 @@
import { msg } from '@lingui/core/macro';
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
import {
ActorMetadata,
FieldMetadataType,
RelationOnDeleteAction,
} from 'twenty-shared/types';
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
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';
@@ -49,7 +49,8 @@ export class AttachmentWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Attachment name`,
icon: 'IconFileUpload',
})
name: string;
@WorkspaceIsNullable()
name: string | null;
@WorkspaceField({
standardId: ATTACHMENT_STANDARD_FIELD_IDS.fullPath,
@@ -58,7 +59,8 @@ export class AttachmentWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Attachment full path`,
icon: 'IconLink',
})
fullPath: string;
@WorkspaceIsNullable()
fullPath: string | null;
// Deprecated: Use fileCategory instead
@WorkspaceField({
@@ -69,7 +71,7 @@ export class AttachmentWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconList',
})
@WorkspaceIsNullable()
type: string;
type: string | null;
@WorkspaceField({
standardId: ATTACHMENT_STANDARD_FIELD_IDS.fileCategory,
@@ -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';
@@ -8,6 +8,7 @@ import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/i
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
@@ -34,7 +35,8 @@ export class BlocklistWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Handle`,
icon: 'IconAt',
})
handle: string;
@WorkspaceIsNullable()
handle: string | null;
@WorkspaceRelation({
standardId: BLOCKLIST_STANDARD_FIELD_IDS.workspaceMember,
@@ -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 BlocklistItemDeleteCalendarEventsJob {
acc.set(workspaceMemberId, []);
}
if (!isDefined(handle)) {
return acc;
}
acc.get(workspaceMemberId)?.push(handle);
return acc;
@@ -1,5 +1,7 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { CalDAVClient } from 'src/modules/calendar/calendar-event-import-manager/drivers/caldav/lib/caldav.client';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
@@ -13,7 +15,8 @@ export class CalDavClientProvider {
): Promise<CalDAVClient> {
if (
!connectedAccount.connectionParameters?.CALDAV?.password ||
!connectedAccount.connectionParameters?.CALDAV?.host
!connectedAccount.connectionParameters?.CALDAV?.host ||
!isDefined(connectedAccount.handle)
) {
throw new Error('Missing required CalDAV connection parameters');
}
@@ -9,4 +9,6 @@ export enum CalendarEventImportDriverExceptionCode {
SYNC_CURSOR_ERROR = 'SYNC_CURSOR_ERROR',
UNKNOWN = 'UNKNOWN',
UNKNOWN_NETWORK_ERROR = 'UNKNOWN_NETWORK_ERROR',
HANDLE_ALIASES_REQUIRED = 'HANDLE_ALIASES_REQUIRED',
CHANNEL_MISCONFIGURED = 'CHANNEL_MISCONFIGURED',
}
@@ -67,6 +67,7 @@ export class CalendarEventImportErrorHandlerService {
case CalendarEventImportDriverExceptionCode.SYNC_CURSOR_ERROR:
await this.handleSyncCursorErrorException(calendarChannel, workspaceId);
break;
case CalendarEventImportDriverExceptionCode.CHANNEL_MISCONFIGURED:
case CalendarEventImportDriverExceptionCode.UNKNOWN:
case CalendarEventImportDriverExceptionCode.UNKNOWN_NETWORK_ERROR:
default:
@@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { Any } from 'typeorm';
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
@@ -11,6 +12,10 @@ import { BlocklistRepository } from 'src/modules/blocklist/repositories/blocklis
import { BlocklistWorkspaceEntity } from 'src/modules/blocklist/standard-objects/blocklist.workspace-entity';
import { CalendarEventCleanerService } from 'src/modules/calendar/calendar-event-cleaner/services/calendar-event-cleaner.service';
import { CALENDAR_EVENT_IMPORT_BATCH_SIZE } from 'src/modules/calendar/calendar-event-import-manager/constants/calendar-event-import-batch-size';
import {
CalendarEventImportDriverException,
CalendarEventImportDriverExceptionCode,
} from 'src/modules/calendar/calendar-event-import-manager/drivers/exceptions/calendar-event-import-driver.exception';
import { MicrosoftCalendarImportEventsService } from 'src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/services/microsoft-calendar-import-events.service';
import {
CalendarEventImportErrorHandlerService,
@@ -92,6 +97,16 @@ export class CalendarEventsImportService {
workspaceId,
);
if (
!isDefined(connectedAccount.handleAliases) ||
!isDefined(calendarChannel.handle)
) {
throw new CalendarEventImportDriverException(
'Calendar channel handle or Handle aliases are required',
CalendarEventImportDriverExceptionCode.CHANNEL_MISCONFIGURED,
);
}
const { filteredEvents, cancelledEvents } =
filterEventsAndReturnCancelledEvents(
[
@@ -99,7 +114,7 @@ export class CalendarEventsImportService {
...connectedAccount.handleAliases.split(','),
],
calendarEvents,
blocklist.map((blocklist) => blocklist.handle),
blocklist.map((blocklist) => blocklist.handle ?? ''),
);
const cancelledEventExternalIds = cancelledEvents.map(
@@ -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';
@@ -58,6 +60,13 @@ export class CalendarFetchEventsService {
refreshToken,
};
if (!isDefined(calendarChannel.syncCursor)) {
throw new CalendarEventImportDriverException(
'Sync cursor is required',
CalendarEventImportDriverExceptionCode.SYNC_CURSOR_ERROR,
);
}
const getCalendarEventsResponse =
await this.getCalendarEventsService.getCalendarEvents(
connectedAccountWithFreshTokens,
@@ -5,7 +5,7 @@ export const mapCalendarEventsByICalUID = (
): Map<string, string> => {
return new Map<string, string>(
existingCalendarEvents.map((calendarEvent) => [
calendarEvent.iCalUID,
calendarEvent.iCalUID ?? '',
calendarEvent.id,
]),
);
@@ -3,8 +3,8 @@ import { Injectable } from '@nestjs/common';
import { isDefined } from 'class-validator';
import chunk from 'lodash.chunk';
import differenceWith from 'lodash.differencewith';
import { Any } from 'typeorm';
import { FieldActorSource } from 'twenty-shared/types';
import { Any } from 'typeorm';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
@@ -158,7 +158,10 @@ export class CalendarEventParticipantService {
{
workspaceId,
connectedAccount,
contactsToCreate: savedParticipants,
contactsToCreate: savedParticipants.map((participant) => ({
handle: participant.handle ?? '',
displayName: participant.displayName ?? participant.handle ?? '',
})),
source: FieldActorSource.CALENDAR,
},
);
@@ -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';
@@ -9,6 +9,7 @@ import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
@@ -42,7 +43,8 @@ export class CalendarChannelEventAssociationWorkspaceEntity extends BaseWorkspac
description: msg`Event external ID`,
icon: 'IconCalendar',
})
eventExternalId: string;
@WorkspaceIsNullable()
eventExternalId: string | null;
@WorkspaceField({
standardId:
@@ -52,7 +54,8 @@ export class CalendarChannelEventAssociationWorkspaceEntity extends BaseWorkspac
description: msg`Recurring Event ID`,
icon: 'IconHistory',
})
recurringEventExternalId: string;
@WorkspaceIsNullable()
recurringEventExternalId: string | null;
@WorkspaceRelation({
standardId:
@@ -1,8 +1,8 @@
import { registerEnumType } from '@nestjs/graphql';
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';
@@ -87,7 +87,8 @@ export class CalendarChannelWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Handle`,
icon: 'IconAt',
})
handle: string;
@WorkspaceIsNullable()
handle: string | null;
@WorkspaceField({
standardId: CALENDAR_CHANNEL_STANDARD_FIELD_IDS.syncStatus,
@@ -280,7 +281,8 @@ export class CalendarChannelWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Sync Cursor. Used for syncing events from the calendar provider`,
icon: 'IconReload',
})
syncCursor: string;
@WorkspaceIsNullable()
syncCursor: string | null;
@WorkspaceField({
standardId: CALENDAR_CHANNEL_STANDARD_FIELD_IDS.syncedAt,
@@ -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';
@@ -47,7 +47,8 @@ export class CalendarEventParticipantWorkspaceEntity extends BaseWorkspaceEntity
description: msg`Handle`,
icon: 'IconMail',
})
handle: string;
@WorkspaceIsNullable()
handle: string | null;
@WorkspaceField({
standardId: CALENDAR_EVENT_PARTICIPANT_STANDARD_FIELD_IDS.displayName,
@@ -56,7 +57,8 @@ export class CalendarEventParticipantWorkspaceEntity extends BaseWorkspaceEntity
description: msg`Display Name`,
icon: 'IconUser',
})
displayName: string;
@WorkspaceIsNullable()
displayName: string | null;
@WorkspaceField({
standardId: CALENDAR_EVENT_PARTICIPANT_STANDARD_FIELD_IDS.isOrganizer,
@@ -1,10 +1,10 @@
import { msg } from '@lingui/core/macro';
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
import {
FieldMetadataType,
RelationOnDeleteAction,
LinksMetadata,
RelationOnDeleteAction,
} from 'twenty-shared/types';
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
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';
@@ -43,7 +43,8 @@ export class CalendarEventWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Title`,
icon: 'IconH1',
})
title: string;
@WorkspaceIsNullable()
title: string | null;
@WorkspaceField({
standardId: CALENDAR_EVENT_STANDARD_FIELD_IDS.isCanceled,
@@ -112,7 +113,8 @@ export class CalendarEventWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Description`,
icon: 'IconFileDescription',
})
description: string;
@WorkspaceIsNullable()
description: string | null;
@WorkspaceField({
standardId: CALENDAR_EVENT_STANDARD_FIELD_IDS.location,
@@ -121,7 +123,8 @@ export class CalendarEventWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Location`,
icon: 'IconMapPin',
})
location: string;
@WorkspaceIsNullable()
location: string | null;
@WorkspaceField({
standardId: CALENDAR_EVENT_STANDARD_FIELD_IDS.iCalUID,
@@ -130,7 +133,8 @@ export class CalendarEventWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`iCal UID`,
icon: 'IconKey',
})
iCalUID: string;
@WorkspaceIsNullable()
iCalUID: string | null;
@WorkspaceField({
standardId: CALENDAR_EVENT_STANDARD_FIELD_IDS.conferenceSolution,
@@ -139,7 +143,8 @@ export class CalendarEventWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Conference Solution`,
icon: 'IconScreenShare',
})
conferenceSolution: string;
@WorkspaceIsNullable()
conferenceSolution: string | null;
@WorkspaceField({
standardId: CALENDAR_EVENT_STANDARD_FIELD_IDS.conferenceLink,
@@ -1,4 +1,5 @@
import { msg } from '@lingui/core/macro';
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
import {
ActorMetadata,
AddressMetadata,
@@ -7,13 +8,12 @@ import {
RelationOnDeleteAction,
type CurrencyMetadata,
} from 'twenty-shared/types';
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
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';
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { WorkspaceDuplicateCriteria } from 'src/engine/twenty-orm/decorators/workspace-duplicate-criteria.decorator';
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
@@ -72,7 +72,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconBuildingSkyscraper',
})
@WorkspaceIsNullable()
name: string;
name: string | null;
@WorkspaceField({
standardId: COMPANY_STANDARD_FIELD_IDS.domainName,
@@ -282,7 +282,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity {
})
@WorkspaceIsDeprecated()
@WorkspaceIsNullable()
addressOld: string;
addressOld: string | null;
@WorkspaceField({
standardId: COMPANY_STANDARD_FIELD_IDS.searchVector,
@@ -0,0 +1,4 @@
export enum OAuth2ClientManagerExceptionCode {
ACCESS_TOKEN_REQUIRED = 'ACCESS_TOKEN_REQUIRED',
REFRESH_TOKEN_REQUIRED = 'REFRESH_TOKEN_REQUIRED',
}
@@ -2,9 +2,11 @@ import { Injectable } from '@nestjs/common';
import { type Client } from '@microsoft/microsoft-graph-client';
import { type Auth } from 'googleapis';
import { CustomError, isDefined } from 'twenty-shared/utils';
import { GoogleOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/google/google-oauth2-client-manager.service';
import { MicrosoftOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service';
import { OAuth2ClientManagerExceptionCode } from 'src/modules/connected-account/oauth2-client-manager/exceptions/oauth2-client-manager.exceptions';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
@Injectable()
@@ -20,6 +22,13 @@ export class OAuth2ClientManagerService {
'provider' | 'refreshToken'
>,
): Promise<Auth.OAuth2Client> {
if (!isDefined(connectedAccount.refreshToken)) {
throw new CustomError(
'Refresh token is required',
OAuth2ClientManagerExceptionCode.REFRESH_TOKEN_REQUIRED,
);
}
return this.googleOAuth2ClientManagerService.getOAuth2Client(
connectedAccount.refreshToken,
);
@@ -31,6 +40,13 @@ export class OAuth2ClientManagerService {
'provider' | 'accessToken'
>,
): Promise<Client> {
if (!isDefined(connectedAccount.accessToken)) {
throw new CustomError(
'Access token is required',
OAuth2ClientManagerExceptionCode.ACCESS_TOKEN_REQUIRED,
);
}
return this.microsoftOAuth2ClientManagerService.getOAuth2Client(
connectedAccount.accessToken,
);
@@ -7,4 +7,5 @@ export enum ConnectedAccountRefreshAccessTokenExceptionCode {
INVALID_REFRESH_TOKEN = 'INVALID_REFRESH_TOKEN',
PROVIDER_NOT_SUPPORTED = 'PROVIDER_NOT_SUPPORTED',
TEMPORARY_NETWORK_ERROR = 'TEMPORARY_NETWORK_ERROR',
ACCESS_TOKEN_NOT_FOUND = 'ACCESS_TOKEN_NOT_FOUND',
}
@@ -1,7 +1,7 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConnectedAccountProvider } from 'twenty-shared/types';
import { assertUnreachable } from 'twenty-shared/utils';
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { GoogleAPIRefreshAccessTokenService } from 'src/modules/connected-account/refresh-tokens-manager/drivers/google/services/google-api-refresh-tokens.service';
@@ -52,6 +52,12 @@ export class ConnectedAccountRefreshTokensService {
this.logger.debug(
`Reusing valid access token for connected account ${connectedAccount.id.slice(0, 7)} in workspace ${workspaceId.slice(0, 7)}`,
);
if (!isDefined(accessToken)) {
throw new ConnectedAccountRefreshAccessTokenException(
`Access token is required for connected account ${connectedAccount.id} in workspace ${workspaceId}`,
ConnectedAccountRefreshAccessTokenExceptionCode.ACCESS_TOKEN_NOT_FOUND,
);
}
return {
accessToken,
@@ -1,10 +1,10 @@
import { msg } from '@lingui/core/macro';
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
import {
ConnectedAccountProvider,
FieldMetadataType,
RelationOnDeleteAction,
} from 'twenty-shared/types';
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
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';
@@ -42,7 +42,8 @@ export class ConnectedAccountWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`The account handle (email, username, phone number, etc.)`,
icon: 'IconMail',
})
handle: string;
@WorkspaceIsNullable()
handle: string | null;
@WorkspaceField({
standardId: CONNECTED_ACCOUNT_STANDARD_FIELD_IDS.provider,
@@ -50,6 +51,7 @@ export class ConnectedAccountWorkspaceEntity extends BaseWorkspaceEntity {
label: msg`provider`,
description: msg`The account provider`,
icon: 'IconSettings',
defaultValue: `'${ConnectedAccountProvider.GOOGLE}'`,
})
provider: ConnectedAccountProvider; // field metadata should be a SELECT
@@ -60,7 +62,8 @@ export class ConnectedAccountWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Messaging provider access token`,
icon: 'IconKey',
})
accessToken: string;
@WorkspaceIsNullable()
accessToken: string | null;
@WorkspaceField({
standardId: CONNECTED_ACCOUNT_STANDARD_FIELD_IDS.refreshToken,
@@ -69,7 +72,8 @@ export class ConnectedAccountWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Messaging provider refresh token`,
icon: 'IconKey',
})
refreshToken: string;
@WorkspaceIsNullable()
refreshToken: string | null;
@WorkspaceField({
standardId: CONNECTED_ACCOUNT_STANDARD_FIELD_IDS.lastCredentialsRefreshedAt,
@@ -88,7 +92,8 @@ export class ConnectedAccountWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Last sync history ID`,
icon: 'IconHistory',
})
lastSyncHistoryId: string;
@WorkspaceIsNullable()
lastSyncHistoryId: string | null;
@WorkspaceField({
standardId: CONNECTED_ACCOUNT_STANDARD_FIELD_IDS.authFailedAt,
@@ -107,7 +112,8 @@ export class ConnectedAccountWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Handle Aliases`,
icon: 'IconMail',
})
handleAliases: string;
@WorkspaceIsNullable()
handleAliases: string | null;
@WorkspaceField({
standardId: CONNECTED_ACCOUNT_STANDARD_FIELD_IDS.scopes,
@@ -1,3 +1,5 @@
import { isDefined } from 'twenty-shared/utils';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type Contact } from 'src/modules/contact-creation-manager/types/contact.type';
import { getDomainNameFromHandle } from 'src/modules/contact-creation-manager/utils/get-domain-name-from-handle.util';
@@ -9,6 +11,9 @@ export function filterOutContactsThatBelongToSelfOrWorkspaceMembers(
connectedAccount: ConnectedAccountWorkspaceEntity,
workspaceMembers: WorkspaceMemberWorkspaceEntity[],
): Contact[] {
if (!isDefined(connectedAccount.handle)) {
throw new Error('Connected account handle is missing');
}
const selfDomainName = getDomainNameFromHandle(
connectedAccount.handle,
).toLowerCase();
@@ -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';
@@ -12,6 +12,7 @@ import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
import { FAVORITE_FOLDER_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
@WorkspaceEntity({
standardId: STANDARD_OBJECT_IDS.favoriteFolder,
@@ -42,7 +43,8 @@ export class FavoriteFolderWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Name of the favorite folder`,
icon: 'IconText',
})
name: string;
@WorkspaceIsNullable()
name: string | null;
@WorkspaceRelation({
standardId: FAVORITE_FOLDER_STANDARD_FIELD_IDS.favorites,
@@ -119,7 +119,7 @@ export class MatchParticipantService<
for (const participants of chunkedParticipants) {
const uniqueParticipantsHandles = [
...new Set(participants.map((participant) => participant.handle)),
];
].filter(isDefined);
const queryBuilder = addPersonEmailFiltersToQueryBuilder({
queryBuilder: personRepository.createQueryBuilder('person'),
@@ -140,6 +140,10 @@ export class MatchParticipantService<
);
const partipantsToBeUpdated = participants
.map((participant) => ({
...participant,
handle: participant.handle ?? '',
}))
.map((participant) => {
const person = findPersonByPrimaryOrAdditionalEmail({
people,
@@ -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,
);
@@ -1,4 +1,5 @@
import { msg } from '@lingui/core/macro';
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
import {
ActorMetadata,
EmailsMetadata,
@@ -8,13 +9,12 @@ import {
type FullNameMetadata,
type LinksMetadata,
} from 'twenty-shared/types';
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
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';
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { WorkspaceDuplicateCriteria } from 'src/engine/twenty-orm/decorators/workspace-duplicate-criteria.decorator';
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
@@ -127,7 +127,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconBriefcase',
})
@WorkspaceIsNullable()
jobTitle: string;
jobTitle: string | null;
@WorkspaceField({
standardId: PERSON_STANDARD_FIELD_IDS.phone,
@@ -137,7 +137,8 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconPhone',
})
@WorkspaceIsDeprecated()
phone: string;
@WorkspaceIsNullable()
phone: string | null;
@WorkspaceField({
standardId: PERSON_STANDARD_FIELD_IDS.phones,
@@ -160,7 +161,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconMap',
})
@WorkspaceIsNullable()
city: string;
city: string | null;
@WorkspaceField({
standardId: PERSON_STANDARD_FIELD_IDS.avatarUrl,
@@ -171,7 +172,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
})
@WorkspaceIsSystem()
@WorkspaceIsNullable()
avatarUrl: string;
avatarUrl: string | null;
@WorkspaceField({
standardId: PERSON_STANDARD_FIELD_IDS.position,
@@ -1,17 +1,17 @@
import { msg } from '@lingui/core/macro';
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
import {
ActorMetadata,
FieldMetadataType,
RelationOnDeleteAction,
type RichTextV2Metadata,
} from 'twenty-shared/types';
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
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';
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
@@ -75,7 +75,7 @@ export class TaskWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconNotes',
})
@WorkspaceIsNullable()
title: string;
title: string | null;
@WorkspaceField({
standardId: TASK_STANDARD_FIELD_IDS.bodyV2,
@@ -58,7 +58,8 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Event name`,
icon: 'IconAbc',
})
name: string;
@WorkspaceIsNullable()
name: string | null;
@WorkspaceField({
standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.properties,
@@ -78,7 +79,8 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Cached record name`,
icon: 'IconAbc',
})
linkedRecordCachedName: string;
@WorkspaceIsNullable()
linkedRecordCachedName: string | null;
@WorkspaceField({
standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.linkedRecordId,
@@ -1,19 +1,19 @@
import { registerEnumType } from '@nestjs/graphql';
import { msg } from '@lingui/core/macro';
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
import {
ActorMetadata,
FieldMetadataType,
RelationOnDeleteAction,
} from 'twenty-shared/types';
import { type WorkflowRunStepInfos } from 'twenty-shared/workflow';
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
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';
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
@@ -103,7 +103,8 @@ export class WorkflowRunWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Name of the workflow run`,
icon: 'IconSettingsAutomation',
})
name: string;
@WorkspaceIsNullable()
name: string | null;
@WorkspaceField({
standardId: WORKFLOW_RUN_STANDARD_FIELD_IDS.enqueuedAt,
@@ -1,13 +1,13 @@
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';
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
import { type FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
@@ -90,7 +90,8 @@ export class WorkflowVersionWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`The workflow version name`,
icon: 'IconSettingsAutomation',
})
name: string;
@WorkspaceIsNullable()
name: string | null;
@WorkspaceField({
standardId: WORKFLOW_VERSION_STANDARD_FIELD_IDS.trigger,
@@ -126,6 +126,7 @@ export class WorkflowVersionWorkspaceService {
return {
...draftWorkflowVersion,
name: draftWorkflowVersion.name ?? '',
steps: newWorkflowVersionSteps,
trigger: newWorkflowVersionTrigger,
};
@@ -289,6 +290,7 @@ export class WorkflowVersionWorkspaceService {
return {
...newDraftVersion,
name: newDraftVersion.name ?? '',
steps: remappedSteps,
trigger: remappedTrigger ?? null,
};
@@ -1,10 +1,10 @@
import { Injectable } from '@nestjs/common';
import { type ActorMetadata } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { StepStatus, type WorkflowRunStepInfo } from 'twenty-shared/workflow';
import { type QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
import { v4 } from 'uuid';
import { type ActorMetadata } from 'twenty-shared/types';
import { WithLock } from 'src/engine/core-modules/cache-lock/with-lock.decorator';
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
@@ -109,7 +109,7 @@ export class WorkflowRunWorkspaceService {
order: { createdAt: 'desc' },
});
const workflowRunCountMatch = lastWorkflowRun?.name.match(/#(\d+)/);
const workflowRunCountMatch = lastWorkflowRun?.name?.match(/#(\d+)/);
const workflowRunCount = workflowRunCountMatch
? parseInt(workflowRunCountMatch[1], 10)
@@ -1,20 +1,20 @@
import { registerEnumType } from '@nestjs/graphql';
import { msg } from '@lingui/core/macro';
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
import { type APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
import {
FieldMetadataType,
FullNameMetadata,
NumberDataType,
RelationOnDeleteAction,
FullNameMetadata,
} from 'twenty-shared/types';
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
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';
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
@@ -148,8 +148,9 @@ export class WorkspaceMemberWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Workspace member avatar`,
icon: 'IconFileUpload',
})
@WorkspaceIsNullable()
@WorkspaceIsSystem()
avatarUrl: string;
avatarUrl: string | null;
@WorkspaceIsUnique()
@WorkspaceField({
@@ -159,8 +160,9 @@ export class WorkspaceMemberWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Related user email address`,
icon: 'IconMail',
})
@WorkspaceIsNullable()
@WorkspaceIsSystem()
userEmail: string;
userEmail: string | null;
@WorkspaceField({
standardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.calendarStartDay,