Remove connected account feature flag (#19286)
Co-authored-by: martmull <martmull@hotmail.fr> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions <github-actions@twenty.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@@ -25,7 +25,7 @@ export const metadataModuleFactory = async (
|
||||
cacheStorageService: CacheStorageService,
|
||||
metricsService: MetricsService,
|
||||
i18nService: I18nService,
|
||||
featureFlagService: FeatureFlagService,
|
||||
_featureFlagService: FeatureFlagService,
|
||||
): Promise<YogaDriverConfig> => {
|
||||
const config: YogaDriverConfig = {
|
||||
autoSchemaFile: true,
|
||||
|
||||
-2
@@ -7,7 +7,6 @@ import { WorkspaceQueryHookExplorer } from 'src/engine/api/graphql/workspace-que
|
||||
import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service';
|
||||
import { BlocklistQueryHookModule } from 'src/modules/blocklist/query-hooks/blocklist-query-hook.module';
|
||||
import { CalendarQueryHookModule } from 'src/modules/calendar/common/query-hooks/calendar-query-hook.module';
|
||||
import { ConnectedAccountQueryHookModule } from 'src/modules/connected-account/query-hooks/connected-account-query-hook.module';
|
||||
import { DashboardQueryHookModule } from 'src/modules/dashboard/query-hooks/dashboard-query-hook.module';
|
||||
import { MessagingQueryHookModule } from 'src/modules/messaging/common/query-hooks/messaging-query-hook.module';
|
||||
import { NoteQueryHookModule } from 'src/modules/note/query-hooks/note-query-hook.module';
|
||||
@@ -18,7 +17,6 @@ import { WorkspaceMemberQueryHookModule } from 'src/modules/workspace-member/que
|
||||
imports: [
|
||||
MessagingQueryHookModule,
|
||||
CalendarQueryHookModule,
|
||||
ConnectedAccountQueryHookModule,
|
||||
DashboardQueryHookModule,
|
||||
BlocklistQueryHookModule,
|
||||
WorkspaceMemberQueryHookModule,
|
||||
|
||||
+4
-2
@@ -1,13 +1,15 @@
|
||||
import { HealthIndicatorService } from '@nestjs/terminus';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
|
||||
import {
|
||||
CalendarChannelSyncStatus,
|
||||
MessageChannelSyncStatus,
|
||||
} from 'twenty-shared/types';
|
||||
import { HEALTH_ERROR_MESSAGES } from 'src/engine/core-modules/admin-panel/constants/health-error-messages.constants';
|
||||
import { HEALTH_INDICATORS_TIMEOUT } from 'src/engine/core-modules/admin-panel/constants/health-indicators-timeout.conts';
|
||||
import { METRICS_FAILURE_RATE_THRESHOLD } from 'src/engine/core-modules/admin-panel/constants/metrics-failure-rate-threshold.const';
|
||||
import { ConnectedAccountHealth } from 'src/engine/core-modules/admin-panel/indicators/connected-account.health';
|
||||
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
|
||||
import { CalendarChannelSyncStatus } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
import { MessageChannelSyncStatus } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
|
||||
describe('ConnectedAccountHealth', () => {
|
||||
let service: ConnectedAccountHealth;
|
||||
|
||||
@@ -58,10 +58,9 @@ import { UserModule } from 'src/engine/core-modules/user/user.module';
|
||||
import { WorkspaceInvitationModule } from 'src/engine/core-modules/workspace-invitation/workspace-invitation.module';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { CoreEntityCacheModule } from 'src/engine/core-entity-cache/core-entity-cache.module';
|
||||
import { CalendarChannelDataAccessModule } from 'src/engine/metadata-modules/calendar-channel/data-access/calendar-channel-data-access.module';
|
||||
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module';
|
||||
import { MessageChannelDataAccessModule } from 'src/engine/metadata-modules/message-channel/data-access/message-channel-data-access.module';
|
||||
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
@@ -94,15 +93,14 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
|
||||
TwoFactorAuthenticationMethodEntity,
|
||||
ObjectMetadataEntity,
|
||||
ConnectedAccountEntity,
|
||||
MessageChannelEntity,
|
||||
CalendarChannelEntity,
|
||||
]),
|
||||
UserWorkspaceModule,
|
||||
OnboardingModule,
|
||||
ConnectedAccountModule,
|
||||
MessagingCommonModule,
|
||||
MessagingFolderSyncManagerModule,
|
||||
CalendarChannelDataAccessModule,
|
||||
ConnectedAccountDataAccessModule,
|
||||
MessageChannelDataAccessModule,
|
||||
WorkspaceSSOModule,
|
||||
FeatureFlagModule,
|
||||
WorkspaceInvitationModule,
|
||||
|
||||
+3
-2
@@ -101,8 +101,9 @@ export class GoogleAPIsAuthController {
|
||||
const connectedAccountId =
|
||||
await this.googleAPIsService.refreshGoogleRefreshToken({
|
||||
handle,
|
||||
workspaceMemberId: workspaceMemberId,
|
||||
workspaceId: workspaceId,
|
||||
userId,
|
||||
workspaceMemberId,
|
||||
workspaceId,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
calendarVisibility,
|
||||
|
||||
+2
-1
@@ -108,7 +108,8 @@ export class MicrosoftAPIsAuthController {
|
||||
const connectedAccountId =
|
||||
await this.microsoftAPIsService.refreshMicrosoftRefreshToken({
|
||||
handle,
|
||||
workspaceMemberId: workspaceMemberId,
|
||||
userId,
|
||||
workspaceMemberId,
|
||||
workspaceId: workspaceId,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
|
||||
@@ -9,11 +9,7 @@ import { addMilliseconds } from 'date-fns';
|
||||
import ms from 'ms';
|
||||
import { PasswordUpdateNotifyEmail } from 'twenty-emails';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import {
|
||||
AppPath,
|
||||
ConnectedAccountProvider,
|
||||
FeatureFlagKey,
|
||||
} from 'twenty-shared/types';
|
||||
import { AppPath, ConnectedAccountProvider } from 'twenty-shared/types';
|
||||
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
|
||||
@@ -1081,19 +1077,6 @@ export class AuthService {
|
||||
oidcTokenClaims?: Record<string, unknown>;
|
||||
connectedAccountProvider?: ConnectedAccountProvider;
|
||||
}): Promise<void> {
|
||||
const isConnectedAccountMigrated =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED,
|
||||
input.workspaceId,
|
||||
);
|
||||
|
||||
// const willBeEnabledByDefault = DEFAULT_FEATURE_FLAGS.includes(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED);
|
||||
const willBeEnabledByDefault = false;
|
||||
|
||||
if (!isConnectedAccountMigrated && !willBeEnabledByDefault) {
|
||||
return;
|
||||
}
|
||||
|
||||
const provider =
|
||||
input.connectedAccountProvider ??
|
||||
this.mapAuthProviderToConnectedAccountProvider(input.authProvider);
|
||||
|
||||
+20
-24
@@ -1,31 +1,30 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { v4 } from 'uuid';
|
||||
import { EntityManager } from 'typeorm';
|
||||
|
||||
import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service';
|
||||
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
import {
|
||||
CalendarChannelSyncStage,
|
||||
CalendarChannelSyncStatus,
|
||||
CalendarChannelVisibility,
|
||||
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
} from 'twenty-shared/types';
|
||||
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
|
||||
export type CreateCalendarChannelInput = {
|
||||
workspaceId: string;
|
||||
connectedAccountId: string;
|
||||
handle: string;
|
||||
calendarVisibility?: CalendarChannelVisibility;
|
||||
manager: WorkspaceEntityManager;
|
||||
skipMessageChannelConfiguration?: boolean;
|
||||
transactionManager: EntityManager;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class CreateCalendarChannelService {
|
||||
constructor(
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService,
|
||||
) {}
|
||||
|
||||
async createCalendarChannel(
|
||||
@@ -36,8 +35,8 @@ export class CreateCalendarChannelService {
|
||||
connectedAccountId,
|
||||
handle,
|
||||
calendarVisibility,
|
||||
manager,
|
||||
skipMessageChannelConfiguration,
|
||||
transactionManager,
|
||||
} = input;
|
||||
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
@@ -46,23 +45,20 @@ export class CreateCalendarChannelService {
|
||||
async () => {
|
||||
const newCalendarChannelId = v4();
|
||||
|
||||
await this.calendarChannelDataAccessService.save(
|
||||
await transactionManager.getRepository(CalendarChannelEntity).save({
|
||||
id: newCalendarChannelId,
|
||||
connectedAccountId,
|
||||
handle,
|
||||
visibility:
|
||||
calendarVisibility || CalendarChannelVisibility.SHARE_EVERYTHING,
|
||||
syncStatus: skipMessageChannelConfiguration
|
||||
? CalendarChannelSyncStatus.ONGOING
|
||||
: CalendarChannelSyncStatus.NOT_SYNCED,
|
||||
syncStage: skipMessageChannelConfiguration
|
||||
? CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING
|
||||
: CalendarChannelSyncStage.PENDING_CONFIGURATION,
|
||||
workspaceId,
|
||||
{
|
||||
id: newCalendarChannelId,
|
||||
connectedAccountId,
|
||||
handle,
|
||||
visibility:
|
||||
calendarVisibility || CalendarChannelVisibility.SHARE_EVERYTHING,
|
||||
syncStatus: skipMessageChannelConfiguration
|
||||
? CalendarChannelSyncStatus.ONGOING
|
||||
: CalendarChannelSyncStatus.NOT_SYNCED,
|
||||
syncStage: skipMessageChannelConfiguration
|
||||
? CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING
|
||||
: CalendarChannelSyncStage.PENDING_CONFIGURATION,
|
||||
},
|
||||
manager,
|
||||
);
|
||||
} as CalendarChannelEntity);
|
||||
|
||||
return newCalendarChannelId;
|
||||
},
|
||||
|
||||
+42
-12
@@ -1,11 +1,14 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { type ConnectedAccountProvider } from 'twenty-shared/types';
|
||||
import { EntityManager, Repository } from 'typeorm';
|
||||
|
||||
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
|
||||
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
export type CreateConnectedAccountInput = {
|
||||
workspaceId: string;
|
||||
@@ -16,14 +19,15 @@ export type CreateConnectedAccountInput = {
|
||||
refreshToken: string;
|
||||
accountOwnerId: string;
|
||||
scopes: string[];
|
||||
manager: WorkspaceEntityManager;
|
||||
transactionManager: EntityManager;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class CreateConnectedAccountService {
|
||||
constructor(
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
|
||||
@InjectRepository(UserWorkspaceEntity)
|
||||
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
|
||||
) {}
|
||||
|
||||
async createConnectedAccount(
|
||||
@@ -38,25 +42,51 @@ export class CreateConnectedAccountService {
|
||||
refreshToken,
|
||||
accountOwnerId,
|
||||
scopes,
|
||||
manager,
|
||||
} = input;
|
||||
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
await this.connectedAccountDataAccessService.save(
|
||||
workspaceId,
|
||||
{
|
||||
const workspaceMemberRepo =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workspaceMember',
|
||||
);
|
||||
|
||||
const member = await workspaceMemberRepo.findOne({
|
||||
where: { id: accountOwnerId },
|
||||
});
|
||||
|
||||
if (!member) {
|
||||
throw new Error(
|
||||
`Workspace member not found for accountOwnerId ${accountOwnerId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const userWorkspace = await this.userWorkspaceRepository.findOne({
|
||||
where: { userId: member.userId, workspaceId },
|
||||
});
|
||||
|
||||
if (!userWorkspace) {
|
||||
throw new Error(
|
||||
`User workspace not found for user ${member.userId} in workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const userWorkspaceId = userWorkspace.id;
|
||||
|
||||
await input.transactionManager
|
||||
.getRepository(ConnectedAccountEntity)
|
||||
.save({
|
||||
id: connectedAccountId,
|
||||
handle,
|
||||
provider,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
accountOwnerId,
|
||||
userWorkspaceId,
|
||||
scopes,
|
||||
},
|
||||
manager,
|
||||
);
|
||||
workspaceId,
|
||||
} as ConnectedAccountEntity);
|
||||
}, authContext);
|
||||
}
|
||||
}
|
||||
|
||||
+23
-46
@@ -1,37 +1,32 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
import { EntityManager } from 'typeorm';
|
||||
|
||||
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
|
||||
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
import {
|
||||
MessageChannelPendingGroupEmailsAction,
|
||||
MessageChannelSyncStage,
|
||||
MessageChannelSyncStatus,
|
||||
MessageChannelType,
|
||||
MessageChannelVisibility,
|
||||
type MessageChannelWorkspaceEntity,
|
||||
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import { SyncMessageFoldersService } from 'src/modules/messaging/message-folder-manager/services/sync-message-folders.service';
|
||||
} from 'twenty-shared/types';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
|
||||
export type CreateMessageChannelInput = {
|
||||
workspaceId: string;
|
||||
connectedAccountId: string;
|
||||
handle: string;
|
||||
messageVisibility?: MessageChannelVisibility;
|
||||
manager: WorkspaceEntityManager;
|
||||
skipMessageChannelConfiguration?: boolean;
|
||||
transactionManager: EntityManager;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class CreateMessageChannelService {
|
||||
constructor(
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
|
||||
private readonly syncMessageFoldersService: SyncMessageFoldersService,
|
||||
) {}
|
||||
|
||||
async createMessageChannel(
|
||||
@@ -42,50 +37,32 @@ export class CreateMessageChannelService {
|
||||
connectedAccountId,
|
||||
handle,
|
||||
messageVisibility,
|
||||
manager,
|
||||
skipMessageChannelConfiguration,
|
||||
transactionManager,
|
||||
} = input;
|
||||
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
async () => {
|
||||
const messageChannelRepo =
|
||||
transactionManager.getRepository(MessageChannelEntity);
|
||||
const newMessageChannelId = v4();
|
||||
|
||||
await this.messageChannelDataAccessService.save(
|
||||
workspaceId,
|
||||
{
|
||||
id: newMessageChannelId,
|
||||
connectedAccountId,
|
||||
type: MessageChannelType.EMAIL,
|
||||
handle,
|
||||
visibility:
|
||||
messageVisibility || MessageChannelVisibility.SHARE_EVERYTHING,
|
||||
syncStatus: skipMessageChannelConfiguration
|
||||
? MessageChannelSyncStatus.ONGOING
|
||||
: MessageChannelSyncStatus.NOT_SYNCED,
|
||||
syncStage: skipMessageChannelConfiguration
|
||||
? MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING
|
||||
: MessageChannelSyncStage.PENDING_CONFIGURATION,
|
||||
pendingGroupEmailsAction:
|
||||
MessageChannelPendingGroupEmailsAction.NONE,
|
||||
},
|
||||
manager,
|
||||
);
|
||||
|
||||
const createdMessageChannel =
|
||||
await this.messageChannelDataAccessService.findOne(workspaceId, {
|
||||
where: { id: newMessageChannelId },
|
||||
relations: ['connectedAccount', 'messageFolders'],
|
||||
});
|
||||
|
||||
if (!isDefined(createdMessageChannel)) {
|
||||
throw new Error('Message channel not found');
|
||||
}
|
||||
|
||||
await this.syncMessageFoldersService.syncMessageFolders({
|
||||
messageChannel:
|
||||
createdMessageChannel as unknown as MessageChannelWorkspaceEntity,
|
||||
await messageChannelRepo.save({
|
||||
id: newMessageChannelId,
|
||||
connectedAccountId,
|
||||
type: MessageChannelType.EMAIL,
|
||||
handle,
|
||||
visibility:
|
||||
messageVisibility || MessageChannelVisibility.SHARE_EVERYTHING,
|
||||
syncStatus: skipMessageChannelConfiguration
|
||||
? MessageChannelSyncStatus.ONGOING
|
||||
: MessageChannelSyncStatus.NOT_SYNCED,
|
||||
syncStage: skipMessageChannelConfiguration
|
||||
? MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING
|
||||
: MessageChannelSyncStage.PENDING_CONFIGURATION,
|
||||
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction.NONE,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
|
||||
+54
-33
@@ -1,7 +1,12 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
||||
import {
|
||||
CalendarChannelSyncStage,
|
||||
CalendarChannelVisibility,
|
||||
ConnectedAccountProvider,
|
||||
MessageChannelVisibility,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { CreateCalendarChannelService } from 'src/engine/core-modules/auth/services/create-calendar-channel.service';
|
||||
import { CreateConnectedAccountService } from 'src/engine/core-modules/auth/services/create-connected-account.service';
|
||||
@@ -11,23 +16,19 @@ import { GoogleApisServiceAvailabilityService } from 'src/engine/core-modules/au
|
||||
import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-apis.service';
|
||||
import { UpdateConnectedAccountOnReconnectService } from 'src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service';
|
||||
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
|
||||
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { getQueueToken } from 'src/engine/core-modules/message-queue/utils/get-queue-token.util';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service';
|
||||
import {
|
||||
CalendarChannelSyncStage,
|
||||
CalendarChannelVisibility,
|
||||
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service';
|
||||
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service';
|
||||
import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import { SyncMessageFoldersService } from 'src/modules/messaging/message-folder-manager/services/sync-message-folders.service';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: jest.fn(() => 'mocked-uuid'),
|
||||
@@ -39,24 +40,36 @@ describe('GoogleAPIsService', () => {
|
||||
let messagingChannelSyncStatusService: MessageChannelSyncStatusService;
|
||||
let createMessageChannelService: CreateMessageChannelService;
|
||||
|
||||
const mockConnectedAccountDataAccessService = {
|
||||
const mockConnectedAccountRepository = {
|
||||
findOne: jest.fn(),
|
||||
};
|
||||
|
||||
const mockMessageChannelDataAccessService = {
|
||||
const mockTransactionManager = {
|
||||
getRepository: jest.fn().mockReturnValue({ save: jest.fn() }),
|
||||
};
|
||||
|
||||
const mockMessageChannelRepository = {
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn().mockResolvedValue(null),
|
||||
manager: {
|
||||
transaction: jest.fn((callback) => callback(mockTransactionManager)),
|
||||
},
|
||||
};
|
||||
|
||||
const mockCalendarChannelRepository = {
|
||||
find: jest.fn(),
|
||||
};
|
||||
|
||||
const mockCalendarChannelDataAccessService = {
|
||||
find: jest.fn(),
|
||||
const mockUserWorkspaceRepository = {
|
||||
findOne: jest.fn().mockResolvedValue({ id: 'user-workspace-id' }),
|
||||
};
|
||||
|
||||
const mockWorkspaceMemberRepository = {
|
||||
findOneOrFail: jest.fn(),
|
||||
};
|
||||
|
||||
const mockWorkspaceDataSource = {
|
||||
transaction: jest.fn((callback) => callback({})),
|
||||
findOne: jest.fn().mockResolvedValue({
|
||||
id: 'workspace-member-id',
|
||||
userId: 'user-id',
|
||||
}),
|
||||
};
|
||||
|
||||
const mockTwentyConfigService = {
|
||||
@@ -86,9 +99,6 @@ describe('GoogleAPIsService', () => {
|
||||
|
||||
return {};
|
||||
}),
|
||||
getGlobalWorkspaceDataSource: jest
|
||||
.fn()
|
||||
.mockResolvedValue(mockWorkspaceDataSource),
|
||||
executeInWorkspaceContext: jest
|
||||
.fn()
|
||||
.mockImplementation((fn: () => any, _authContext?: any) => fn()),
|
||||
@@ -180,16 +190,26 @@ describe('GoogleAPIsService', () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ConnectedAccountDataAccessService,
|
||||
useValue: mockConnectedAccountDataAccessService,
|
||||
provide: SyncMessageFoldersService,
|
||||
useValue: {
|
||||
syncMessageFolders: jest.fn().mockResolvedValue([]),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: MessageChannelDataAccessService,
|
||||
useValue: mockMessageChannelDataAccessService,
|
||||
provide: getRepositoryToken(ConnectedAccountEntity),
|
||||
useValue: mockConnectedAccountRepository,
|
||||
},
|
||||
{
|
||||
provide: CalendarChannelDataAccessService,
|
||||
useValue: mockCalendarChannelDataAccessService,
|
||||
provide: getRepositoryToken(UserWorkspaceEntity),
|
||||
useValue: mockUserWorkspaceRepository,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(MessageChannelEntity),
|
||||
useValue: mockMessageChannelRepository,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(CalendarChannelEntity),
|
||||
useValue: mockCalendarChannelRepository,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
@@ -220,15 +240,15 @@ describe('GoogleAPIsService', () => {
|
||||
const existingConnectedAccount = {
|
||||
id: 'existing-account-id',
|
||||
handle: 'test@example.com',
|
||||
accountOwnerId: 'workspace-member-id',
|
||||
userWorkspaceId: 'user-workspace-id',
|
||||
provider: ConnectedAccountProvider.GOOGLE,
|
||||
} as ConnectedAccountWorkspaceEntity;
|
||||
} as ConnectedAccountEntity;
|
||||
|
||||
mockConnectedAccountDataAccessService.findOne.mockResolvedValue(
|
||||
mockConnectedAccountRepository.findOne.mockResolvedValue(
|
||||
existingConnectedAccount,
|
||||
);
|
||||
|
||||
mockWorkspaceMemberRepository.findOneOrFail.mockResolvedValue({
|
||||
mockWorkspaceMemberRepository.findOne.mockResolvedValue({
|
||||
id: 'workspace-member-id',
|
||||
userId: 'user-id',
|
||||
});
|
||||
@@ -240,14 +260,15 @@ describe('GoogleAPIsService', () => {
|
||||
syncStage: CalendarChannelSyncStage.FAILED,
|
||||
};
|
||||
|
||||
mockCalendarChannelDataAccessService.find.mockResolvedValue([
|
||||
mockCalendarChannelRepository.find.mockResolvedValue([
|
||||
failedCalendarChannel,
|
||||
]);
|
||||
|
||||
mockMessageChannelDataAccessService.find.mockResolvedValue([]);
|
||||
mockMessageChannelRepository.find.mockResolvedValue([]);
|
||||
|
||||
await service.refreshGoogleRefreshToken({
|
||||
handle: 'test@example.com',
|
||||
userId: 'user-id',
|
||||
workspaceMemberId: 'workspace-member-id',
|
||||
workspaceId: 'workspace-id',
|
||||
accessToken: 'new-access-token',
|
||||
|
||||
+104
-66
@@ -1,7 +1,17 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { ConnectedAccountProvider, FeatureFlagKey } from 'twenty-shared/types';
|
||||
import {
|
||||
CalendarChannelSyncStage,
|
||||
type CalendarChannelVisibility,
|
||||
ConnectedAccountProvider,
|
||||
FeatureFlagKey,
|
||||
MessageChannelSyncStage,
|
||||
MessageChannelSyncStatus,
|
||||
type MessageChannelVisibility,
|
||||
} from 'twenty-shared/types';
|
||||
import { v4 } from 'uuid';
|
||||
import { EntityManager, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
AuthException,
|
||||
@@ -14,14 +24,15 @@ import { GoogleAPIScopesService } from 'src/engine/core-modules/auth/services/go
|
||||
import { GoogleApisServiceAvailabilityService } from 'src/engine/core-modules/auth/services/google-apis-service-availability.service';
|
||||
import { UpdateConnectedAccountOnReconnectService } from 'src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { SyncMessageFoldersService } from 'src/modules/messaging/message-folder-manager/services/sync-message-folders.service';
|
||||
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';
|
||||
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service';
|
||||
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
|
||||
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
|
||||
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
import {
|
||||
@@ -29,23 +40,14 @@ import {
|
||||
type CalendarEventListFetchJobData,
|
||||
} from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job';
|
||||
import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service';
|
||||
import {
|
||||
CalendarChannelSyncStage,
|
||||
type CalendarChannelVisibility,
|
||||
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service';
|
||||
|
||||
import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service';
|
||||
import {
|
||||
MessageChannelSyncStage,
|
||||
MessageChannelSyncStatus,
|
||||
type MessageChannelVisibility,
|
||||
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import {
|
||||
MessagingMessageListFetchJob,
|
||||
type MessagingMessageListFetchJobData,
|
||||
} from 'src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
@Injectable()
|
||||
export class GoogleAPIsService {
|
||||
@@ -66,13 +68,20 @@ export class GoogleAPIsService {
|
||||
private readonly googleAPIScopesService: GoogleAPIScopesService,
|
||||
private readonly googleApisServiceAvailabilityService: GoogleApisServiceAvailabilityService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
|
||||
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
|
||||
private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService,
|
||||
private readonly syncMessageFoldersService: SyncMessageFoldersService,
|
||||
@InjectRepository(ConnectedAccountEntity)
|
||||
private readonly connectedAccountRepository: Repository<ConnectedAccountEntity>,
|
||||
@InjectRepository(UserWorkspaceEntity)
|
||||
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
|
||||
@InjectRepository(MessageChannelEntity)
|
||||
private readonly messageChannelRepository: Repository<MessageChannelEntity>,
|
||||
@InjectRepository(CalendarChannelEntity)
|
||||
private readonly calendarChannelRepository: Repository<CalendarChannelEntity>,
|
||||
) {}
|
||||
|
||||
async refreshGoogleRefreshToken(input: {
|
||||
handle: string;
|
||||
userId: string;
|
||||
workspaceMemberId: string;
|
||||
workspaceId: string;
|
||||
accessToken: string;
|
||||
@@ -84,6 +93,7 @@ export class GoogleAPIsService {
|
||||
const {
|
||||
handle,
|
||||
workspaceId,
|
||||
userId,
|
||||
workspaceMemberId,
|
||||
calendarVisibility,
|
||||
messageVisibility,
|
||||
@@ -132,19 +142,48 @@ export class GoogleAPIsService {
|
||||
|
||||
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
async () => {
|
||||
const connectedAccount =
|
||||
await this.connectedAccountDataAccessService.findOne(workspaceId, {
|
||||
where: { handle, accountOwnerId: workspaceMemberId },
|
||||
});
|
||||
const userWorkspace = await this.userWorkspaceRepository.findOne({
|
||||
where: { userId, workspaceId },
|
||||
});
|
||||
|
||||
if (!isDefined(userWorkspace)) {
|
||||
throw new AuthException(
|
||||
`User workspace not found for user ${userId} in workspace ${workspaceId}`,
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
const userWorkspaceId = userWorkspace.id;
|
||||
|
||||
const connectedAccount = await this.connectedAccountRepository.findOne({
|
||||
where: {
|
||||
handle,
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
const existingAccountId = connectedAccount?.id;
|
||||
const newOrExistingConnectedAccountId = existingAccountId ?? v4();
|
||||
|
||||
const workspaceDataSource =
|
||||
await this.globalWorkspaceOrmManager.getGlobalWorkspaceDataSource();
|
||||
const existingMessageChannels =
|
||||
await this.messageChannelRepository.find({
|
||||
where: {
|
||||
connectedAccountId: newOrExistingConnectedAccountId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
await workspaceDataSource.transaction(
|
||||
async (manager: WorkspaceEntityManager) => {
|
||||
const existingCalendarChannels =
|
||||
await this.calendarChannelRepository.find({
|
||||
where: {
|
||||
connectedAccountId: newOrExistingConnectedAccountId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
await this.messageChannelRepository.manager.transaction(
|
||||
async (transactionManager: EntityManager) => {
|
||||
await this.createConnectedAccountService.createConnectedAccount({
|
||||
workspaceId,
|
||||
connectedAccountId: newOrExistingConnectedAccountId,
|
||||
@@ -154,7 +193,7 @@ export class GoogleAPIsService {
|
||||
refreshToken: input.refreshToken,
|
||||
accountOwnerId: workspaceMemberId,
|
||||
scopes,
|
||||
manager,
|
||||
transactionManager,
|
||||
});
|
||||
|
||||
if (existingAccountId) {
|
||||
@@ -165,24 +204,10 @@ export class GoogleAPIsService {
|
||||
accessToken: input.accessToken,
|
||||
refreshToken: input.refreshToken,
|
||||
scopes,
|
||||
manager,
|
||||
transactionManager,
|
||||
},
|
||||
);
|
||||
|
||||
const workspaceMemberRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workspaceMember',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const workspaceMember =
|
||||
await workspaceMemberRepository.findOneOrFail({
|
||||
where: { id: workspaceMemberId },
|
||||
});
|
||||
|
||||
const userId = workspaceMember.userId;
|
||||
|
||||
await this.accountsToReconnectService.removeAccountToReconnect(
|
||||
userId,
|
||||
workspaceId,
|
||||
@@ -190,11 +215,6 @@ export class GoogleAPIsService {
|
||||
);
|
||||
}
|
||||
|
||||
const existingMessageChannels =
|
||||
await this.messageChannelDataAccessService.find(workspaceId, {
|
||||
connectedAccountId: newOrExistingConnectedAccountId,
|
||||
});
|
||||
|
||||
if (
|
||||
isMessagingEnabled &&
|
||||
isMessagingAvailable &&
|
||||
@@ -205,18 +225,11 @@ export class GoogleAPIsService {
|
||||
connectedAccountId: newOrExistingConnectedAccountId,
|
||||
handle,
|
||||
messageVisibility,
|
||||
manager,
|
||||
skipMessageChannelConfiguration,
|
||||
transactionManager,
|
||||
});
|
||||
}
|
||||
|
||||
const existingCalendarChannels =
|
||||
await this.calendarChannelDataAccessService.find(workspaceId, {
|
||||
where: {
|
||||
connectedAccountId: newOrExistingConnectedAccountId,
|
||||
},
|
||||
});
|
||||
|
||||
if (
|
||||
isCalendarEnabled &&
|
||||
isCalendarAvailable &&
|
||||
@@ -227,18 +240,43 @@ export class GoogleAPIsService {
|
||||
connectedAccountId: newOrExistingConnectedAccountId,
|
||||
handle,
|
||||
calendarVisibility,
|
||||
manager,
|
||||
skipMessageChannelConfiguration,
|
||||
transactionManager,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (isMessagingEnabled) {
|
||||
const messageChannels =
|
||||
await this.messageChannelDataAccessService.find(workspaceId, {
|
||||
connectedAccountId: newOrExistingConnectedAccountId,
|
||||
if (
|
||||
isMessagingEnabled &&
|
||||
isMessagingAvailable &&
|
||||
existingMessageChannels.length === 0
|
||||
) {
|
||||
const newMessageChannel = await this.messageChannelRepository.findOne(
|
||||
{
|
||||
where: {
|
||||
connectedAccountId: newOrExistingConnectedAccountId,
|
||||
workspaceId,
|
||||
},
|
||||
relations: ['connectedAccount', 'messageFolders'],
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(newMessageChannel)) {
|
||||
await this.syncMessageFoldersService.syncMessageFolders({
|
||||
messageChannel: newMessageChannel,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (isMessagingEnabled) {
|
||||
const messageChannels = await this.messageChannelRepository.find({
|
||||
where: {
|
||||
connectedAccountId: newOrExistingConnectedAccountId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!isMessagingAvailable && messageChannels.length > 0) {
|
||||
await this.messagingChannelSyncStatusService.markAsFailed(
|
||||
@@ -268,12 +306,12 @@ export class GoogleAPIsService {
|
||||
}
|
||||
|
||||
if (isCalendarEnabled) {
|
||||
const calendarChannels =
|
||||
await this.calendarChannelDataAccessService.find(workspaceId, {
|
||||
where: {
|
||||
connectedAccountId: newOrExistingConnectedAccountId,
|
||||
},
|
||||
});
|
||||
const calendarChannels = await this.calendarChannelRepository.find({
|
||||
where: {
|
||||
connectedAccountId: newOrExistingConnectedAccountId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!isCalendarAvailable && calendarChannels.length > 0) {
|
||||
await this.calendarChannelSyncStatusService.markAsFailedInsufficientPermissionsAndFlushCalendarEventsToImport(
|
||||
|
||||
+54
-33
@@ -1,30 +1,31 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
||||
import {
|
||||
CalendarChannelSyncStage,
|
||||
CalendarChannelVisibility,
|
||||
ConnectedAccountProvider,
|
||||
MessageChannelVisibility,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { CreateCalendarChannelService } from 'src/engine/core-modules/auth/services/create-calendar-channel.service';
|
||||
import { CreateConnectedAccountService } from 'src/engine/core-modules/auth/services/create-connected-account.service';
|
||||
import { CreateMessageChannelService } from 'src/engine/core-modules/auth/services/create-message-channel.service';
|
||||
import { MicrosoftAPIsService } from 'src/engine/core-modules/auth/services/microsoft-apis.service';
|
||||
import { UpdateConnectedAccountOnReconnectService } from 'src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service';
|
||||
import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service';
|
||||
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
|
||||
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { getQueueToken } from 'src/engine/core-modules/message-queue/utils/get-queue-token.util';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service';
|
||||
import {
|
||||
CalendarChannelSyncStage,
|
||||
CalendarChannelVisibility,
|
||||
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service';
|
||||
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service';
|
||||
import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import { SyncMessageFoldersService } from 'src/modules/messaging/message-folder-manager/services/sync-message-folders.service';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: jest.fn(() => 'mocked-uuid'),
|
||||
@@ -36,24 +37,36 @@ describe('MicrosoftAPIsService', () => {
|
||||
let calendarChannelSyncStatusService: CalendarChannelSyncStatusService;
|
||||
let createMessageChannelService: CreateMessageChannelService;
|
||||
|
||||
const mockConnectedAccountDataAccessService = {
|
||||
const mockConnectedAccountRepository = {
|
||||
findOne: jest.fn(),
|
||||
};
|
||||
|
||||
const mockMessageChannelDataAccessService = {
|
||||
const mockTransactionManager = {
|
||||
getRepository: jest.fn().mockReturnValue({ save: jest.fn() }),
|
||||
};
|
||||
|
||||
const mockMessageChannelRepository = {
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn().mockResolvedValue(null),
|
||||
manager: {
|
||||
transaction: jest.fn((callback) => callback(mockTransactionManager)),
|
||||
},
|
||||
};
|
||||
|
||||
const mockCalendarChannelRepository = {
|
||||
find: jest.fn(),
|
||||
};
|
||||
|
||||
const mockCalendarChannelDataAccessService = {
|
||||
find: jest.fn(),
|
||||
const mockUserWorkspaceRepository = {
|
||||
findOne: jest.fn().mockResolvedValue({ id: 'user-workspace-id' }),
|
||||
};
|
||||
|
||||
const mockWorkspaceMemberRepository = {
|
||||
findOneOrFail: jest.fn(),
|
||||
};
|
||||
|
||||
const mockWorkspaceDataSource = {
|
||||
transaction: jest.fn((callback) => callback({})),
|
||||
findOne: jest.fn().mockResolvedValue({
|
||||
id: 'workspace-member-id',
|
||||
userId: 'user-id',
|
||||
}),
|
||||
};
|
||||
|
||||
const mockTwentyConfigService = {
|
||||
@@ -83,9 +96,6 @@ describe('MicrosoftAPIsService', () => {
|
||||
|
||||
return {};
|
||||
}),
|
||||
getGlobalWorkspaceDataSource: jest
|
||||
.fn()
|
||||
.mockResolvedValue(mockWorkspaceDataSource),
|
||||
executeInWorkspaceContext: jest
|
||||
.fn()
|
||||
.mockImplementation((fn: () => any, _authContext?: any) => fn()),
|
||||
@@ -154,16 +164,26 @@ describe('MicrosoftAPIsService', () => {
|
||||
useValue: mockCalendarQueueService,
|
||||
},
|
||||
{
|
||||
provide: ConnectedAccountDataAccessService,
|
||||
useValue: mockConnectedAccountDataAccessService,
|
||||
provide: getRepositoryToken(ConnectedAccountEntity),
|
||||
useValue: mockConnectedAccountRepository,
|
||||
},
|
||||
{
|
||||
provide: MessageChannelDataAccessService,
|
||||
useValue: mockMessageChannelDataAccessService,
|
||||
provide: getRepositoryToken(UserWorkspaceEntity),
|
||||
useValue: mockUserWorkspaceRepository,
|
||||
},
|
||||
{
|
||||
provide: CalendarChannelDataAccessService,
|
||||
useValue: mockCalendarChannelDataAccessService,
|
||||
provide: getRepositoryToken(MessageChannelEntity),
|
||||
useValue: mockMessageChannelRepository,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(CalendarChannelEntity),
|
||||
useValue: mockCalendarChannelRepository,
|
||||
},
|
||||
{
|
||||
provide: SyncMessageFoldersService,
|
||||
useValue: {
|
||||
syncMessageFolders: jest.fn().mockResolvedValue([]),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
@@ -194,15 +214,15 @@ describe('MicrosoftAPIsService', () => {
|
||||
const existingConnectedAccount = {
|
||||
id: 'existing-account-id',
|
||||
handle: 'test@example.com',
|
||||
accountOwnerId: 'workspace-member-id',
|
||||
userWorkspaceId: 'user-workspace-id',
|
||||
provider: ConnectedAccountProvider.MICROSOFT,
|
||||
} as ConnectedAccountWorkspaceEntity;
|
||||
} as ConnectedAccountEntity;
|
||||
|
||||
mockConnectedAccountDataAccessService.findOne.mockResolvedValue(
|
||||
mockConnectedAccountRepository.findOne.mockResolvedValue(
|
||||
existingConnectedAccount,
|
||||
);
|
||||
|
||||
mockWorkspaceMemberRepository.findOneOrFail.mockResolvedValue({
|
||||
mockWorkspaceMemberRepository.findOne.mockResolvedValue({
|
||||
id: 'workspace-member-id',
|
||||
userId: 'user-id',
|
||||
});
|
||||
@@ -214,11 +234,11 @@ describe('MicrosoftAPIsService', () => {
|
||||
syncStage: CalendarChannelSyncStage.FAILED,
|
||||
};
|
||||
|
||||
mockCalendarChannelDataAccessService.find.mockResolvedValue([
|
||||
mockCalendarChannelRepository.find.mockResolvedValue([
|
||||
failedCalendarChannel,
|
||||
]);
|
||||
|
||||
mockMessageChannelDataAccessService.find.mockResolvedValue([
|
||||
mockMessageChannelRepository.find.mockResolvedValue([
|
||||
{
|
||||
id: 'message-channel-id',
|
||||
connectedAccountId: 'existing-account-id',
|
||||
@@ -227,6 +247,7 @@ describe('MicrosoftAPIsService', () => {
|
||||
|
||||
await service.refreshMicrosoftRefreshToken({
|
||||
handle: 'test@example.com',
|
||||
userId: 'user-id',
|
||||
workspaceMemberId: 'workspace-member-id',
|
||||
workspaceId: 'workspace-id',
|
||||
accessToken: 'new-access-token',
|
||||
|
||||
+106
-64
@@ -1,21 +1,34 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
||||
import {
|
||||
CalendarChannelSyncStage,
|
||||
type CalendarChannelVisibility,
|
||||
ConnectedAccountProvider,
|
||||
MessageChannelSyncStage,
|
||||
type MessageChannelVisibility,
|
||||
} from 'twenty-shared/types';
|
||||
import { v4 } from 'uuid';
|
||||
import { EntityManager, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { CreateCalendarChannelService } from 'src/engine/core-modules/auth/services/create-calendar-channel.service';
|
||||
import { CreateConnectedAccountService } from 'src/engine/core-modules/auth/services/create-connected-account.service';
|
||||
import { CreateMessageChannelService } from 'src/engine/core-modules/auth/services/create-message-channel.service';
|
||||
import { UpdateConnectedAccountOnReconnectService } from 'src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service';
|
||||
import { getMicrosoftApisOauthScopes } from 'src/engine/core-modules/auth/utils/get-microsoft-apis-oauth-scopes';
|
||||
import { SyncMessageFoldersService } from 'src/modules/messaging/message-folder-manager/services/sync-message-folders.service';
|
||||
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';
|
||||
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service';
|
||||
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
|
||||
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
|
||||
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
import {
|
||||
@@ -23,22 +36,14 @@ import {
|
||||
type CalendarEventListFetchJobData,
|
||||
} from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job';
|
||||
import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service';
|
||||
import {
|
||||
CalendarChannelSyncStage,
|
||||
type CalendarChannelVisibility,
|
||||
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service';
|
||||
|
||||
import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service';
|
||||
import {
|
||||
MessageChannelSyncStage,
|
||||
type MessageChannelVisibility,
|
||||
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import {
|
||||
MessagingMessageListFetchJob,
|
||||
type MessagingMessageListFetchJobData,
|
||||
} from 'src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
@Injectable()
|
||||
export class MicrosoftAPIsService {
|
||||
@@ -56,13 +61,20 @@ export class MicrosoftAPIsService {
|
||||
private readonly createConnectedAccountService: CreateConnectedAccountService,
|
||||
private readonly updateConnectedAccountOnReconnectService: UpdateConnectedAccountOnReconnectService,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
|
||||
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
|
||||
private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService,
|
||||
private readonly syncMessageFoldersService: SyncMessageFoldersService,
|
||||
@InjectRepository(ConnectedAccountEntity)
|
||||
private readonly connectedAccountRepository: Repository<ConnectedAccountEntity>,
|
||||
@InjectRepository(UserWorkspaceEntity)
|
||||
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
|
||||
@InjectRepository(MessageChannelEntity)
|
||||
private readonly messageChannelRepository: Repository<MessageChannelEntity>,
|
||||
@InjectRepository(CalendarChannelEntity)
|
||||
private readonly calendarChannelRepository: Repository<CalendarChannelEntity>,
|
||||
) {}
|
||||
|
||||
async refreshMicrosoftRefreshToken(input: {
|
||||
handle: string;
|
||||
userId: string;
|
||||
workspaceMemberId: string;
|
||||
workspaceId: string;
|
||||
accessToken: string;
|
||||
@@ -74,6 +86,7 @@ export class MicrosoftAPIsService {
|
||||
const {
|
||||
handle,
|
||||
workspaceId,
|
||||
userId,
|
||||
workspaceMemberId,
|
||||
calendarVisibility,
|
||||
messageVisibility,
|
||||
@@ -86,19 +99,48 @@ export class MicrosoftAPIsService {
|
||||
|
||||
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
async () => {
|
||||
const connectedAccount =
|
||||
await this.connectedAccountDataAccessService.findOne(workspaceId, {
|
||||
where: { handle, accountOwnerId: workspaceMemberId },
|
||||
});
|
||||
const userWorkspace = await this.userWorkspaceRepository.findOne({
|
||||
where: { userId, workspaceId },
|
||||
});
|
||||
|
||||
if (!isDefined(userWorkspace)) {
|
||||
throw new AuthException(
|
||||
`User workspace not found for user ${userId} in workspace ${workspaceId}`,
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
const userWorkspaceId = userWorkspace.id;
|
||||
|
||||
const connectedAccount = await this.connectedAccountRepository.findOne({
|
||||
where: {
|
||||
handle,
|
||||
userWorkspaceId: userWorkspaceId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
const existingAccountId = connectedAccount?.id;
|
||||
const newOrExistingConnectedAccountId = existingAccountId ?? v4();
|
||||
|
||||
const workspaceDataSource =
|
||||
await this.globalWorkspaceOrmManager.getGlobalWorkspaceDataSource();
|
||||
const existingMessageChannels =
|
||||
await this.messageChannelRepository.find({
|
||||
where: {
|
||||
connectedAccountId: newOrExistingConnectedAccountId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
await workspaceDataSource.transaction(
|
||||
async (manager: WorkspaceEntityManager) => {
|
||||
const existingCalendarChannels =
|
||||
await this.calendarChannelRepository.find({
|
||||
where: {
|
||||
connectedAccountId: newOrExistingConnectedAccountId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
await this.messageChannelRepository.manager.transaction(
|
||||
async (transactionManager: EntityManager) => {
|
||||
await this.createConnectedAccountService.createConnectedAccount({
|
||||
workspaceId,
|
||||
connectedAccountId: newOrExistingConnectedAccountId,
|
||||
@@ -108,7 +150,7 @@ export class MicrosoftAPIsService {
|
||||
refreshToken: input.refreshToken,
|
||||
accountOwnerId: workspaceMemberId,
|
||||
scopes,
|
||||
manager,
|
||||
transactionManager,
|
||||
});
|
||||
|
||||
if (existingAccountId) {
|
||||
@@ -119,24 +161,10 @@ export class MicrosoftAPIsService {
|
||||
accessToken: input.accessToken,
|
||||
refreshToken: input.refreshToken,
|
||||
scopes,
|
||||
manager,
|
||||
transactionManager,
|
||||
},
|
||||
);
|
||||
|
||||
const workspaceMemberRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workspaceMember',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const workspaceMember =
|
||||
await workspaceMemberRepository.findOneOrFail({
|
||||
where: { id: workspaceMemberId },
|
||||
});
|
||||
|
||||
const userId = workspaceMember.userId;
|
||||
|
||||
await this.accountsToReconnectService.removeAccountToReconnect(
|
||||
userId,
|
||||
workspaceId,
|
||||
@@ -154,11 +182,6 @@ export class MicrosoftAPIsService {
|
||||
);
|
||||
}
|
||||
|
||||
const existingMessageChannels =
|
||||
await this.messageChannelDataAccessService.find(workspaceId, {
|
||||
connectedAccountId: newOrExistingConnectedAccountId,
|
||||
});
|
||||
|
||||
if (
|
||||
this.twentyConfigService.get(
|
||||
'MESSAGING_PROVIDER_MICROSOFT_ENABLED',
|
||||
@@ -170,18 +193,11 @@ export class MicrosoftAPIsService {
|
||||
connectedAccountId: newOrExistingConnectedAccountId,
|
||||
handle,
|
||||
messageVisibility,
|
||||
manager,
|
||||
skipMessageChannelConfiguration,
|
||||
transactionManager,
|
||||
});
|
||||
}
|
||||
|
||||
const existingCalendarChannels =
|
||||
await this.calendarChannelDataAccessService.find(workspaceId, {
|
||||
where: {
|
||||
connectedAccountId: newOrExistingConnectedAccountId,
|
||||
},
|
||||
});
|
||||
|
||||
if (
|
||||
this.twentyConfigService.get(
|
||||
'CALENDAR_PROVIDER_MICROSOFT_ENABLED',
|
||||
@@ -193,20 +209,46 @@ export class MicrosoftAPIsService {
|
||||
connectedAccountId: newOrExistingConnectedAccountId,
|
||||
handle,
|
||||
calendarVisibility,
|
||||
manager,
|
||||
skipMessageChannelConfiguration,
|
||||
transactionManager,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (
|
||||
this.twentyConfigService.get(
|
||||
'MESSAGING_PROVIDER_MICROSOFT_ENABLED',
|
||||
) &&
|
||||
existingMessageChannels.length === 0
|
||||
) {
|
||||
const newMessageChannel = await this.messageChannelRepository.findOne(
|
||||
{
|
||||
where: {
|
||||
connectedAccountId: newOrExistingConnectedAccountId,
|
||||
workspaceId,
|
||||
},
|
||||
relations: ['connectedAccount', 'messageFolders'],
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(newMessageChannel)) {
|
||||
await this.syncMessageFoldersService.syncMessageFolders({
|
||||
messageChannel: newMessageChannel,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
this.twentyConfigService.get('MESSAGING_PROVIDER_MICROSOFT_ENABLED')
|
||||
) {
|
||||
const messageChannels =
|
||||
await this.messageChannelDataAccessService.find(workspaceId, {
|
||||
const messageChannels = await this.messageChannelRepository.find({
|
||||
where: {
|
||||
connectedAccountId: newOrExistingConnectedAccountId,
|
||||
});
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
for (const messageChannel of messageChannels) {
|
||||
if (
|
||||
@@ -227,12 +269,12 @@ export class MicrosoftAPIsService {
|
||||
if (
|
||||
this.twentyConfigService.get('CALENDAR_PROVIDER_MICROSOFT_ENABLED')
|
||||
) {
|
||||
const calendarChannels =
|
||||
await this.calendarChannelDataAccessService.find(workspaceId, {
|
||||
where: {
|
||||
connectedAccountId: newOrExistingConnectedAccountId,
|
||||
},
|
||||
});
|
||||
const calendarChannels = await this.calendarChannelRepository.find({
|
||||
where: {
|
||||
connectedAccountId: newOrExistingConnectedAccountId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
for (const calendarChannel of calendarChannels) {
|
||||
if (
|
||||
|
||||
+18
-16
@@ -1,7 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
|
||||
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
|
||||
import { EntityManager } from 'typeorm';
|
||||
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
|
||||
@@ -11,14 +12,13 @@ export type UpdateConnectedAccountOnReconnectInput = {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
scopes: string[];
|
||||
manager: WorkspaceEntityManager;
|
||||
transactionManager: EntityManager;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class UpdateConnectedAccountOnReconnectService {
|
||||
constructor(
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
|
||||
) {}
|
||||
|
||||
async updateConnectedAccountOnReconnect(
|
||||
@@ -35,18 +35,20 @@ export class UpdateConnectedAccountOnReconnectService {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
await this.connectedAccountDataAccessService.update(
|
||||
workspaceId,
|
||||
{
|
||||
id: connectedAccountId,
|
||||
},
|
||||
{
|
||||
accessToken,
|
||||
refreshToken,
|
||||
scopes,
|
||||
authFailedAt: null,
|
||||
},
|
||||
);
|
||||
await input.transactionManager
|
||||
.getRepository(ConnectedAccountEntity)
|
||||
.update(
|
||||
{
|
||||
id: connectedAccountId,
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
accessToken,
|
||||
refreshToken,
|
||||
scopes,
|
||||
authFailedAt: null,
|
||||
},
|
||||
);
|
||||
}, authContext);
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -1,7 +1,9 @@
|
||||
import { type Request } from 'express';
|
||||
|
||||
import { type CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
import { type MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import {
|
||||
type CalendarChannelVisibility,
|
||||
type MessageChannelVisibility,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
export type APIsOAuthRequest = Omit<
|
||||
Request,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { type CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
import { type MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import {
|
||||
CalendarChannelVisibility,
|
||||
MessageChannelVisibility,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
export type APIsOAuthState = {
|
||||
transientToken?: string;
|
||||
|
||||
+4
-2
@@ -1,7 +1,9 @@
|
||||
import {
|
||||
CalendarChannelVisibility,
|
||||
MessageChannelVisibility,
|
||||
} from 'twenty-shared/types';
|
||||
import { type APIsOAuthRequest } from 'src/engine/core-modules/auth/types/apis-oauth-request.type';
|
||||
import { setRequestExtraParams } from 'src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util';
|
||||
import { CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
|
||||
describe('googleApisSetRequestExtraParams', () => {
|
||||
it('should set request extra params', () => {
|
||||
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { CalendarChannelVisibility } from 'twenty-shared/types';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { TimelineCalendarEventParticipantDTO } from 'src/engine/core-modules/calendar/dtos/timeline-calendar-event-participant.dto';
|
||||
import { CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
|
||||
@ObjectType('LinkMetadata')
|
||||
class LinkMetadataDTO {
|
||||
|
||||
+12
-1
@@ -1,11 +1,22 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { TimelineCalendarEventResolver } from 'src/engine/core-modules/calendar/timeline-calendar-event.resolver';
|
||||
import { TimelineCalendarEventService } from 'src/engine/core-modules/calendar/timeline-calendar-event.service';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { UserModule } from 'src/engine/core-modules/user/user.module';
|
||||
|
||||
@Module({
|
||||
imports: [UserModule],
|
||||
imports: [
|
||||
UserModule,
|
||||
TypeOrmModule.forFeature([
|
||||
CalendarChannelEntity,
|
||||
ConnectedAccountEntity,
|
||||
UserWorkspaceEntity,
|
||||
]),
|
||||
],
|
||||
exports: [],
|
||||
providers: [TimelineCalendarEventResolver, TimelineCalendarEventService],
|
||||
})
|
||||
|
||||
+90
-26
@@ -1,10 +1,14 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
|
||||
|
||||
import { CalendarChannelVisibility } from 'twenty-shared/types';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
import { CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
import { type CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity';
|
||||
|
||||
import { TimelineCalendarEventService } from './timeline-calendar-event.service';
|
||||
@@ -19,6 +23,10 @@ type MockWorkspaceRepository = Partial<
|
||||
describe('TimelineCalendarEventService', () => {
|
||||
let service: TimelineCalendarEventService;
|
||||
let mockCalendarEventRepository: MockWorkspaceRepository;
|
||||
let mockCalendarChannelCoreRepository: { find: jest.Mock };
|
||||
let mockConnectedAccountRepository: { find: jest.Mock };
|
||||
let mockUserWorkspaceRepository: { findOne: jest.Mock };
|
||||
let mockWorkspaceMemberRepository: { findOne: jest.Mock };
|
||||
|
||||
const mockCalendarEvent: Partial<CalendarEventWorkspaceEntity> = {
|
||||
id: '1',
|
||||
@@ -36,8 +44,32 @@ describe('TimelineCalendarEventService', () => {
|
||||
findAndCount: jest.fn(),
|
||||
};
|
||||
|
||||
mockConnectedAccountRepository = {
|
||||
find: jest.fn().mockResolvedValue([]),
|
||||
};
|
||||
|
||||
mockCalendarChannelCoreRepository = {
|
||||
find: jest.fn().mockResolvedValue([]),
|
||||
};
|
||||
|
||||
mockUserWorkspaceRepository = {
|
||||
findOne: jest.fn().mockResolvedValue(null),
|
||||
};
|
||||
|
||||
mockWorkspaceMemberRepository = {
|
||||
findOne: jest.fn().mockResolvedValue(null),
|
||||
};
|
||||
|
||||
const mockGlobalWorkspaceOrmManager = {
|
||||
getRepository: jest.fn().mockResolvedValue(mockCalendarEventRepository),
|
||||
getRepository: jest
|
||||
.fn()
|
||||
.mockImplementation((_workspaceId, entityName) => {
|
||||
if (entityName === 'workspaceMember') {
|
||||
return Promise.resolve(mockWorkspaceMemberRepository);
|
||||
}
|
||||
|
||||
return Promise.resolve(mockCalendarEventRepository);
|
||||
}),
|
||||
executeInWorkspaceContext: jest
|
||||
.fn()
|
||||
.mockImplementation((fn: () => any, _authContext?: any) => fn()),
|
||||
@@ -50,6 +82,18 @@ describe('TimelineCalendarEventService', () => {
|
||||
provide: GlobalWorkspaceOrmManager,
|
||||
useValue: mockGlobalWorkspaceOrmManager,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(CalendarChannelEntity),
|
||||
useValue: mockCalendarChannelCoreRepository,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(ConnectedAccountEntity),
|
||||
useValue: mockConnectedAccountRepository,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(UserWorkspaceEntity),
|
||||
useValue: mockUserWorkspaceRepository,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@@ -70,19 +114,21 @@ describe('TimelineCalendarEventService', () => {
|
||||
{
|
||||
...mockCalendarEvent,
|
||||
calendarChannelEventAssociations: [
|
||||
{
|
||||
calendarChannel: {
|
||||
visibility: CalendarChannelVisibility.SHARE_EVERYTHING,
|
||||
connectedAccount: {
|
||||
accountOwnerId: 'other-workspace-member-id',
|
||||
},
|
||||
},
|
||||
},
|
||||
{ calendarChannelId: 'channel-1' },
|
||||
],
|
||||
},
|
||||
],
|
||||
1,
|
||||
]);
|
||||
mockCalendarChannelCoreRepository.find.mockResolvedValue([
|
||||
{
|
||||
id: 'channel-1',
|
||||
visibility: CalendarChannelVisibility.SHARE_EVERYTHING,
|
||||
connectedAccountId: 'connected-account-1',
|
||||
},
|
||||
]);
|
||||
// Ownership doesn't matter for SHARE_EVERYTHING
|
||||
mockWorkspaceMemberRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
const result = await service.getCalendarEventsFromPersonIds({
|
||||
currentWorkspaceMemberId,
|
||||
@@ -110,19 +156,27 @@ describe('TimelineCalendarEventService', () => {
|
||||
{
|
||||
...mockCalendarEvent,
|
||||
calendarChannelEventAssociations: [
|
||||
{
|
||||
calendarChannel: {
|
||||
visibility: CalendarChannelVisibility.METADATA,
|
||||
connectedAccount: {
|
||||
accountOwnerId: 'other-workspace-member-id',
|
||||
},
|
||||
},
|
||||
},
|
||||
{ calendarChannelId: 'channel-1' },
|
||||
],
|
||||
},
|
||||
],
|
||||
1,
|
||||
]);
|
||||
mockCalendarChannelCoreRepository.find.mockResolvedValue([
|
||||
{
|
||||
id: 'channel-1',
|
||||
visibility: CalendarChannelVisibility.METADATA,
|
||||
connectedAccountId: 'connected-account-1',
|
||||
},
|
||||
]);
|
||||
// Current user resolves but doesn't own the account
|
||||
mockWorkspaceMemberRepository.findOne.mockResolvedValue({
|
||||
userId: 'current-user-id',
|
||||
});
|
||||
mockUserWorkspaceRepository.findOne.mockResolvedValue({
|
||||
id: 'current-uw-id',
|
||||
});
|
||||
mockConnectedAccountRepository.find.mockResolvedValue([]);
|
||||
|
||||
const result = await service.getCalendarEventsFromPersonIds({
|
||||
currentWorkspaceMemberId,
|
||||
@@ -152,19 +206,29 @@ describe('TimelineCalendarEventService', () => {
|
||||
{
|
||||
...mockCalendarEvent,
|
||||
calendarChannelEventAssociations: [
|
||||
{
|
||||
calendarChannel: {
|
||||
visibility: CalendarChannelVisibility.METADATA,
|
||||
connectedAccount: {
|
||||
accountOwnerId: 'current-workspace-member-id',
|
||||
},
|
||||
},
|
||||
},
|
||||
{ calendarChannelId: 'channel-1' },
|
||||
],
|
||||
},
|
||||
],
|
||||
1,
|
||||
]);
|
||||
mockCalendarChannelCoreRepository.find.mockResolvedValue([
|
||||
{
|
||||
id: 'channel-1',
|
||||
visibility: CalendarChannelVisibility.METADATA,
|
||||
connectedAccountId: 'connected-account-1',
|
||||
},
|
||||
]);
|
||||
// Current user resolves and owns the account
|
||||
mockWorkspaceMemberRepository.findOne.mockResolvedValue({
|
||||
userId: 'current-user-id',
|
||||
});
|
||||
mockUserWorkspaceRepository.findOne.mockResolvedValue({
|
||||
id: 'current-uw-id',
|
||||
});
|
||||
mockConnectedAccountRepository.find.mockResolvedValue([
|
||||
{ id: 'connected-account-1' },
|
||||
]);
|
||||
|
||||
const result = await service.getCalendarEventsFromPersonIds({
|
||||
currentWorkspaceMemberId,
|
||||
|
||||
+102
-22
@@ -1,22 +1,33 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import omit from 'lodash.omit';
|
||||
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
|
||||
import { Any } from 'typeorm';
|
||||
import { Any, In, type Repository } from 'typeorm';
|
||||
|
||||
import { CalendarChannelVisibility } from 'twenty-shared/types';
|
||||
import { TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE } from 'src/engine/core-modules/calendar/constants/calendar.constants';
|
||||
import { type TimelineCalendarEventsWithTotalDTO } from 'src/engine/core-modules/calendar/dtos/timeline-calendar-events-with-total.dto';
|
||||
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
import { CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
import { type CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity';
|
||||
import { type OpportunityWorkspaceEntity } from 'src/modules/opportunity/standard-objects/opportunity.workspace-entity';
|
||||
import { type PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
|
||||
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class TimelineCalendarEventService {
|
||||
constructor(
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
@InjectRepository(CalendarChannelEntity)
|
||||
private readonly calendarChannelRepository: Repository<CalendarChannelEntity>,
|
||||
@InjectRepository(ConnectedAccountEntity)
|
||||
private readonly connectedAccountRepository: Repository<ConnectedAccountEntity>,
|
||||
@InjectRepository(UserWorkspaceEntity)
|
||||
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
|
||||
) {}
|
||||
|
||||
async getCalendarEventsFromPersonIds({
|
||||
@@ -79,16 +90,83 @@ export class TimelineCalendarEventService {
|
||||
person: true,
|
||||
workspaceMember: true,
|
||||
},
|
||||
calendarChannelEventAssociations: {
|
||||
calendarChannel: {
|
||||
connectedAccount: {
|
||||
accountOwner: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
calendarChannelEventAssociations: true,
|
||||
},
|
||||
});
|
||||
|
||||
const allCalendarChannelIds = [
|
||||
...new Set(
|
||||
events.flatMap((event) =>
|
||||
event.calendarChannelEventAssociations.map(
|
||||
(association) => association.calendarChannelId,
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
const calendarChannels =
|
||||
allCalendarChannelIds.length > 0
|
||||
? await this.calendarChannelRepository.find({
|
||||
where: { id: In(allCalendarChannelIds), workspaceId },
|
||||
})
|
||||
: [];
|
||||
|
||||
// Resolve current user's userWorkspaceId (workspaceMember → userId → userWorkspace)
|
||||
const workspaceMemberRepo =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workspaceMember',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const currentMember = await workspaceMemberRepo.findOne({
|
||||
where: { id: currentWorkspaceMemberId },
|
||||
select: { userId: true },
|
||||
});
|
||||
|
||||
const currentUserWorkspaceId = currentMember
|
||||
? ((
|
||||
await this.userWorkspaceRepository.findOne({
|
||||
where: { userId: currentMember.userId, workspaceId },
|
||||
select: { id: true },
|
||||
})
|
||||
)?.id ?? null)
|
||||
: null;
|
||||
|
||||
// Find which connected accounts the current user owns (1 query)
|
||||
const connectedAccountIds = [
|
||||
...new Set(
|
||||
calendarChannels.map((channel) => channel.connectedAccountId),
|
||||
),
|
||||
];
|
||||
|
||||
const ownedAccountIds =
|
||||
connectedAccountIds.length > 0 && currentUserWorkspaceId
|
||||
? new Set(
|
||||
(
|
||||
await this.connectedAccountRepository.find({
|
||||
where: {
|
||||
id: In(connectedAccountIds),
|
||||
userWorkspaceId: currentUserWorkspaceId,
|
||||
},
|
||||
select: { id: true },
|
||||
})
|
||||
).map((a) => a.id),
|
||||
)
|
||||
: new Set<string>();
|
||||
|
||||
const calendarChannelMap = new Map(
|
||||
calendarChannels.map((channel) => [
|
||||
channel.id,
|
||||
{
|
||||
visibility: channel.visibility,
|
||||
isOwnedByCurrentUser: ownedAccountIds.has(
|
||||
channel.connectedAccountId,
|
||||
),
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const orderedEvents = events.sort(
|
||||
(a, b) => ids.indexOf(a.id) - ids.indexOf(b.id),
|
||||
);
|
||||
@@ -123,20 +201,22 @@ export class TimelineCalendarEventService {
|
||||
}),
|
||||
);
|
||||
|
||||
const isCalendarEventImportedByCurrentWorkspaceMember =
|
||||
event.calendarChannelEventAssociations.some(
|
||||
(association) =>
|
||||
association.calendarChannel.connectedAccount.accountOwnerId ===
|
||||
currentWorkspaceMemberId,
|
||||
);
|
||||
const hasFullAccess = event.calendarChannelEventAssociations.some(
|
||||
(association) => {
|
||||
const channel = calendarChannelMap.get(
|
||||
association.calendarChannelId,
|
||||
);
|
||||
|
||||
const visibility =
|
||||
event.calendarChannelEventAssociations.some(
|
||||
(association) =>
|
||||
association.calendarChannel.visibility === 'SHARE_EVERYTHING',
|
||||
) || isCalendarEventImportedByCurrentWorkspaceMember
|
||||
? CalendarChannelVisibility.SHARE_EVERYTHING
|
||||
: CalendarChannelVisibility.METADATA;
|
||||
return (
|
||||
channel?.visibility === 'SHARE_EVERYTHING' ||
|
||||
channel?.isOwnedByCurrentUser
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const visibility = hasFullAccess
|
||||
? CalendarChannelVisibility.SHARE_EVERYTHING
|
||||
: CalendarChannelVisibility.METADATA;
|
||||
|
||||
return {
|
||||
...omit(event, [
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ export class ConnectedImapSmtpCaldavAccountDTO {
|
||||
provider: ConnectedAccountProvider;
|
||||
|
||||
@Field(() => UUIDScalarType)
|
||||
accountOwnerId: string;
|
||||
userWorkspaceId: string;
|
||||
|
||||
@Field(() => ImapSmtpCaldavConnectionParametersDTO, { nullable: true })
|
||||
connectionParameters: ImapSmtpCaldavConnectionParametersDTO | null;
|
||||
|
||||
+3
-2
@@ -1,9 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { ImapSmtpCaldavValidatorModule } from 'src/engine/core-modules/imap-smtp-caldav-connection/services/imap-smtp-caldav-connection-validator.module';
|
||||
import { MessageQueueModule } from 'src/engine/core-modules/message-queue/message-queue.module';
|
||||
import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module';
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
|
||||
@@ -22,7 +23,7 @@ import { ImapSmtpCaldavService } from './services/imap-smtp-caldav-connection.se
|
||||
IMAPAPIsModule,
|
||||
MessagingImportManagerModule,
|
||||
MessageQueueModule,
|
||||
ConnectedAccountDataAccessModule,
|
||||
TypeOrmModule.forFeature([ConnectedAccountEntity]),
|
||||
TwentyORMModule,
|
||||
FeatureFlagModule,
|
||||
ImapSmtpCaldavValidatorModule,
|
||||
|
||||
+1
-1
@@ -57,7 +57,7 @@ export class ImapSmtpCaldavResolver {
|
||||
handle: connectedAccount.handle,
|
||||
provider: connectedAccount.provider,
|
||||
connectionParameters: connectedAccount.connectionParameters,
|
||||
accountOwnerId: connectedAccount.accountOwnerId,
|
||||
userWorkspaceId: connectedAccount.userWorkspaceId,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+14
-12
@@ -1,12 +1,14 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { ImapFlow } from 'imapflow';
|
||||
import { createTransport } from 'nodemailer';
|
||||
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import {
|
||||
type AccountType,
|
||||
type ConnectionParameters,
|
||||
@@ -14,7 +16,6 @@ import {
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
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';
|
||||
|
||||
@Injectable()
|
||||
export class ImapSmtpCaldavService {
|
||||
@@ -22,7 +23,8 @@ export class ImapSmtpCaldavService {
|
||||
|
||||
constructor(
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
|
||||
@InjectRepository(ConnectedAccountEntity)
|
||||
private readonly connectedAccountRepository: Repository<ConnectedAccountEntity>,
|
||||
) {}
|
||||
|
||||
async testImapConnection(
|
||||
@@ -185,20 +187,20 @@ export class ImapSmtpCaldavService {
|
||||
async getImapSmtpCaldav(
|
||||
workspaceId: string,
|
||||
connectionId: string,
|
||||
): Promise<ConnectedAccountWorkspaceEntity | null> {
|
||||
): Promise<ConnectedAccountEntity | null> {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
async () => {
|
||||
const connectedAccount =
|
||||
await this.connectedAccountDataAccessService.findOne(workspaceId, {
|
||||
where: {
|
||||
id: connectionId,
|
||||
provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
|
||||
},
|
||||
});
|
||||
const connectedAccount = await this.connectedAccountRepository.findOne({
|
||||
where: {
|
||||
id: connectionId,
|
||||
provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
return connectedAccount as ConnectedAccountWorkspaceEntity | null;
|
||||
return connectedAccount;
|
||||
},
|
||||
authContext,
|
||||
);
|
||||
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { MessageChannelVisibility } from 'twenty-shared/types';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { TimelineThreadParticipantDTO } from 'src/engine/core-modules/messaging/dtos/timeline-thread-participant.dto';
|
||||
import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
|
||||
@ObjectType('TimelineThread')
|
||||
export class TimelineThreadDTO {
|
||||
|
||||
+107
-25
@@ -1,19 +1,32 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { MessageParticipantRole } from 'twenty-shared/types';
|
||||
import { In } from 'typeorm';
|
||||
import {
|
||||
MessageChannelVisibility,
|
||||
MessageParticipantRole,
|
||||
} from 'twenty-shared/types';
|
||||
import { In, type Repository } from 'typeorm';
|
||||
|
||||
import { type TimelineThreadDTO } from 'src/engine/core-modules/messaging/dtos/timeline-thread.dto';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import { type MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity';
|
||||
import { type MessageThreadWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-thread.workspace-entity';
|
||||
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class TimelineMessagingService {
|
||||
constructor(
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
@InjectRepository(MessageChannelEntity)
|
||||
private readonly messageChannelRepository: Repository<MessageChannelEntity>,
|
||||
@InjectRepository(ConnectedAccountEntity)
|
||||
private readonly connectedAccountRepository: Repository<ConnectedAccountEntity>,
|
||||
@InjectRepository(UserWorkspaceEntity)
|
||||
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
|
||||
) {}
|
||||
|
||||
public async getAndCountMessageThreads(
|
||||
@@ -212,57 +225,126 @@ export class TimelineMessagingService {
|
||||
|
||||
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
async () => {
|
||||
const workspaceMemberRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workspaceMember',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const currentMember = await workspaceMemberRepository.findOne({
|
||||
where: { id: workspaceMemberId },
|
||||
select: { userId: true },
|
||||
});
|
||||
|
||||
if (!currentMember) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const currentUserWorkspace = await this.userWorkspaceRepository.findOne(
|
||||
{
|
||||
where: { userId: currentMember.userId, workspaceId },
|
||||
select: { id: true },
|
||||
},
|
||||
);
|
||||
|
||||
if (!currentUserWorkspace) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const currentUserWorkspaceId = currentUserWorkspace.id;
|
||||
|
||||
const messageThreadRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<MessageThreadWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'messageThread',
|
||||
);
|
||||
|
||||
const threadVisibility = await messageThreadRepository
|
||||
const threadChannelRows = await messageThreadRepository
|
||||
.createQueryBuilder()
|
||||
.select('messageThread.id', 'id')
|
||||
.addSelect('messageChannel.visibility', 'visibility')
|
||||
.addSelect('connectedAccount.accountOwnerId', 'accountOwnerId')
|
||||
.addSelect(
|
||||
'messageChannelMessageAssociation.messageChannelId',
|
||||
'messageChannelId',
|
||||
)
|
||||
.leftJoin('messageThread.messages', 'message')
|
||||
.leftJoin(
|
||||
'message.messageChannelMessageAssociations',
|
||||
'messageChannelMessageAssociation',
|
||||
)
|
||||
.leftJoin(
|
||||
'messageChannelMessageAssociation.messageChannel',
|
||||
'messageChannel',
|
||||
)
|
||||
.leftJoin('messageChannel.connectedAccount', 'connectedAccount')
|
||||
.where('messageThread.id = ANY(:messageThreadIds)', {
|
||||
messageThreadIds: messageThreadIds,
|
||||
messageThreadIds,
|
||||
})
|
||||
.getRawMany();
|
||||
.getRawMany<{ id: string; messageChannelId: string | null }>();
|
||||
|
||||
const allMessageChannelIds = [
|
||||
...new Set(
|
||||
threadChannelRows
|
||||
.map((row) => row.messageChannelId)
|
||||
.filter((id): id is string => id !== null && id !== undefined),
|
||||
),
|
||||
];
|
||||
|
||||
if (allMessageChannelIds.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const messageChannels = await this.messageChannelRepository.find({
|
||||
where: { id: In(allMessageChannelIds), workspaceId },
|
||||
select: { id: true, visibility: true, connectedAccountId: true },
|
||||
});
|
||||
|
||||
const allConnectedAccountIds = [
|
||||
...new Set(
|
||||
messageChannels.map((channel) => channel.connectedAccountId),
|
||||
),
|
||||
];
|
||||
|
||||
const ownedAccountIds = new Set(
|
||||
(
|
||||
await this.connectedAccountRepository.find({
|
||||
where: {
|
||||
id: In(allConnectedAccountIds),
|
||||
userWorkspaceId: currentUserWorkspaceId,
|
||||
},
|
||||
select: { id: true },
|
||||
})
|
||||
).map((account) => account.id),
|
||||
);
|
||||
|
||||
const channelVisibilityMap = new Map(
|
||||
messageChannels.map((channel) => [
|
||||
channel.id,
|
||||
ownedAccountIds.has(channel.connectedAccountId)
|
||||
? MessageChannelVisibility.SHARE_EVERYTHING
|
||||
: channel.visibility,
|
||||
]),
|
||||
);
|
||||
|
||||
const visibilityValues = Object.values(MessageChannelVisibility);
|
||||
|
||||
const threadVisibilityByThreadId: {
|
||||
[key: string]: MessageChannelVisibility;
|
||||
} = threadVisibility.reduce((threadVisibilityAcc, threadVisibility) => {
|
||||
if (threadVisibility.accountOwnerId === workspaceMemberId) {
|
||||
threadVisibilityAcc[threadVisibility.id] =
|
||||
MessageChannelVisibility.SHARE_EVERYTHING;
|
||||
} = {};
|
||||
|
||||
return threadVisibilityAcc;
|
||||
}
|
||||
for (const { id: threadId, messageChannelId } of threadChannelRows) {
|
||||
if (!messageChannelId) continue;
|
||||
|
||||
threadVisibilityAcc[threadVisibility.id] =
|
||||
const channelVisibility = channelVisibilityMap.get(messageChannelId);
|
||||
|
||||
if (!channelVisibility) continue;
|
||||
|
||||
threadVisibilityByThreadId[threadId] =
|
||||
visibilityValues[
|
||||
Math.max(
|
||||
visibilityValues.indexOf(threadVisibility.visibility),
|
||||
visibilityValues.indexOf(channelVisibility),
|
||||
visibilityValues.indexOf(
|
||||
threadVisibilityAcc[threadVisibility.id] ??
|
||||
threadVisibilityByThreadId[threadId] ??
|
||||
MessageChannelVisibility.METADATA,
|
||||
),
|
||||
)
|
||||
];
|
||||
|
||||
return threadVisibilityAcc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
return threadVisibilityByThreadId;
|
||||
},
|
||||
|
||||
+10
-1
@@ -1,13 +1,17 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { GetMessagesService } from 'src/engine/core-modules/messaging/services/get-messages.service';
|
||||
import { TimelineMessagingService } from 'src/engine/core-modules/messaging/services/timeline-messaging.service';
|
||||
import { TimelineMessagingResolver } from 'src/engine/core-modules/messaging/timeline-messaging.resolver';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { UserModule } from 'src/engine/core-modules/user/user.module';
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -16,6 +20,11 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
|
||||
ConnectedAccountModule,
|
||||
FeatureFlagModule,
|
||||
PermissionsModule,
|
||||
TypeOrmModule.forFeature([
|
||||
MessageChannelEntity,
|
||||
ConnectedAccountEntity,
|
||||
UserWorkspaceEntity,
|
||||
]),
|
||||
],
|
||||
exports: [],
|
||||
providers: [
|
||||
|
||||
+1
-1
@@ -1,9 +1,9 @@
|
||||
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { MessageChannelVisibility } from 'twenty-shared/types';
|
||||
import { type TimelineThreadDTO } from 'src/engine/core-modules/messaging/dtos/timeline-thread.dto';
|
||||
import { extractParticipantSummary } from 'src/engine/core-modules/messaging/utils/extract-participant-summary.util';
|
||||
import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import { type MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity';
|
||||
|
||||
export const formatThreads = (
|
||||
|
||||
@@ -15,7 +15,7 @@ import { HttpTool } from 'src/engine/core-modules/tool/tools/http-tool/http-tool
|
||||
import { NavigateAppTool } from 'src/engine/core-modules/tool/tools/navigate-tool/navigate-app-tool';
|
||||
import { SearchHelpCenterTool } from 'src/engine/core-modules/tool/tools/search-help-center-tool/search-help-center-tool';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module';
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { NavigationMenuItemModule } from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.module';
|
||||
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
||||
import { ViewModule } from 'src/engine/metadata-modules/view/view.module';
|
||||
@@ -26,7 +26,7 @@ import { MessagingSendManagerModule } from 'src/modules/messaging/message-outbou
|
||||
imports: [
|
||||
MessagingImportManagerModule,
|
||||
MessagingSendManagerModule,
|
||||
TypeOrmModule.forFeature([FileEntity]),
|
||||
TypeOrmModule.forFeature([FileEntity, ConnectedAccountEntity]),
|
||||
ApplicationModule,
|
||||
FeatureFlagModule,
|
||||
FileModule,
|
||||
@@ -36,7 +36,6 @@ import { MessagingSendManagerModule } from 'src/modules/messaging/message-outbou
|
||||
ViewModule,
|
||||
NavigationMenuItemModule,
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
ConnectedAccountDataAccessModule,
|
||||
],
|
||||
providers: [
|
||||
HttpTool,
|
||||
|
||||
+19
-17
@@ -20,12 +20,11 @@ import { EmailComposerResult } from 'src/engine/core-modules/tool/tools/email-to
|
||||
import { EmailToolInput } from 'src/engine/core-modules/tool/tools/email-tool/types/email-tool-input.type';
|
||||
import { parseCommaSeparatedEmails } from 'src/engine/core-modules/tool/tools/email-tool/utils/parse-comma-separated-emails.util';
|
||||
import { type ToolExecutionContext } from 'src/engine/core-modules/tool/types/tool-execution-context.type';
|
||||
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
import { MessagingAccountAuthenticationService } from 'src/modules/messaging/message-import-manager/services/messaging-account-authentication.service';
|
||||
import { type MessageAttachment } from 'src/modules/messaging/message-import-manager/types/message';
|
||||
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { parseEmailBody } from 'src/utils/parse-email-body';
|
||||
import { streamToBuffer } from 'src/utils/stream-to-buffer';
|
||||
@Injectable()
|
||||
@@ -34,7 +33,8 @@ export class EmailComposerService {
|
||||
|
||||
constructor(
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
|
||||
@InjectRepository(ConnectedAccountEntity)
|
||||
private readonly connectedAccountRepository: Repository<ConnectedAccountEntity>,
|
||||
private readonly messagingAccountAuthenticationService: MessagingAccountAuthenticationService,
|
||||
@InjectRepository(FileEntity)
|
||||
private readonly fileRepository: Repository<FileEntity>,
|
||||
@@ -56,15 +56,14 @@ export class EmailComposerService {
|
||||
|
||||
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
async () => {
|
||||
const connectedAccount =
|
||||
await this.connectedAccountDataAccessService.findOne(workspaceId, {
|
||||
where: { id: connectedAccountId },
|
||||
relations: {
|
||||
messageChannels: {
|
||||
messageFolders: true,
|
||||
},
|
||||
const connectedAccount = await this.connectedAccountRepository.findOne({
|
||||
where: { id: connectedAccountId, workspaceId },
|
||||
relations: {
|
||||
messageChannels: {
|
||||
messageFolders: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (!isDefined(connectedAccount)) {
|
||||
throw new EmailToolException(
|
||||
@@ -73,7 +72,7 @@ export class EmailComposerService {
|
||||
);
|
||||
}
|
||||
|
||||
return connectedAccount as unknown as ConnectedAccountWorkspaceEntity;
|
||||
return connectedAccount;
|
||||
},
|
||||
authContext,
|
||||
);
|
||||
@@ -86,8 +85,9 @@ export class EmailComposerService {
|
||||
|
||||
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
async () => {
|
||||
const allAccounts =
|
||||
await this.connectedAccountDataAccessService.find(workspaceId);
|
||||
const allAccounts = await this.connectedAccountRepository.find({
|
||||
where: { workspaceId },
|
||||
});
|
||||
|
||||
if (!allAccounts || allAccounts.length === 0) {
|
||||
throw new EmailToolException(
|
||||
@@ -271,20 +271,22 @@ export class EmailComposerService {
|
||||
);
|
||||
}
|
||||
|
||||
const connectedAccountAsWorkspaceEntity = connectedAccount;
|
||||
|
||||
const { accessToken, refreshToken } =
|
||||
await this.messagingAccountAuthenticationService.validateAndRefreshConnectedAccountAuthentication(
|
||||
{
|
||||
connectedAccount,
|
||||
connectedAccount: connectedAccountAsWorkspaceEntity,
|
||||
workspaceId,
|
||||
messageChannelId: messageChannel.id,
|
||||
},
|
||||
);
|
||||
|
||||
const connectedAccountWithFreshTokens = {
|
||||
...connectedAccount,
|
||||
...connectedAccountAsWorkspaceEntity,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
} as unknown as ConnectedAccountWorkspaceEntity;
|
||||
};
|
||||
|
||||
const attachments = await this.getAttachments(files || [], workspaceId);
|
||||
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { type MessageAttachment } from 'src/modules/messaging/message-import-manager/types/message';
|
||||
|
||||
export type ComposedEmail = {
|
||||
@@ -8,6 +8,6 @@ export type ComposedEmail = {
|
||||
plainTextBody: string;
|
||||
sanitizedHtmlBody: string;
|
||||
attachments: MessageAttachment[];
|
||||
connectedAccount: ConnectedAccountWorkspaceEntity;
|
||||
connectedAccount: ConnectedAccountEntity;
|
||||
inReplyTo?: string;
|
||||
};
|
||||
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service';
|
||||
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
|
||||
import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([CalendarChannelEntity]),
|
||||
FeatureFlagModule,
|
||||
ConnectedAccountDataAccessModule,
|
||||
],
|
||||
providers: [CalendarChannelDataAccessService],
|
||||
exports: [CalendarChannelDataAccessService],
|
||||
})
|
||||
export class CalendarChannelDataAccessModule {}
|
||||
-311
@@ -1,311 +0,0 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
import {
|
||||
type FindManyOptions,
|
||||
type FindOneOptions,
|
||||
type FindOptionsWhere,
|
||||
Repository,
|
||||
} from 'typeorm';
|
||||
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
|
||||
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
|
||||
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { type CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class CalendarChannelDataAccessService {
|
||||
private readonly logger = new Logger(CalendarChannelDataAccessService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(CalendarChannelEntity)
|
||||
private readonly coreRepository: Repository<CalendarChannelEntity>,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
|
||||
) {}
|
||||
|
||||
private async isMigrated(workspaceId: string): Promise<boolean> {
|
||||
return this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
private async toCoreWhere(
|
||||
workspaceId: string,
|
||||
where: Record<string, unknown>,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const coreWhere: Record<string, unknown> = { ...where, workspaceId };
|
||||
|
||||
if (
|
||||
coreWhere.connectedAccount &&
|
||||
typeof coreWhere.connectedAccount === 'object'
|
||||
) {
|
||||
const connectedAccountWhere = {
|
||||
...(coreWhere.connectedAccount as Record<string, unknown>),
|
||||
};
|
||||
|
||||
if ('accountOwnerId' in connectedAccountWhere) {
|
||||
const { accountOwnerId, ...restConnectedAccount } =
|
||||
connectedAccountWhere;
|
||||
|
||||
const resolvedConnectedAccounts =
|
||||
await this.connectedAccountDataAccessService.find(workspaceId, {
|
||||
accountOwnerId,
|
||||
} as never);
|
||||
|
||||
if (resolvedConnectedAccounts.length > 0) {
|
||||
coreWhere.connectedAccountId = resolvedConnectedAccounts[0].id;
|
||||
} else {
|
||||
coreWhere.connectedAccountId = '00000000-0000-0000-0000-000000000000';
|
||||
}
|
||||
|
||||
if (Object.keys(restConnectedAccount).length > 0) {
|
||||
coreWhere.connectedAccount = restConnectedAccount;
|
||||
} else {
|
||||
delete coreWhere.connectedAccount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return coreWhere;
|
||||
}
|
||||
|
||||
async getWorkspaceRepository(workspaceId: string) {
|
||||
return this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'calendarChannel',
|
||||
);
|
||||
}
|
||||
|
||||
async findOne(
|
||||
workspaceId: string,
|
||||
options: FindOneOptions<CalendarChannelWorkspaceEntity>,
|
||||
): Promise<CalendarChannelWorkspaceEntity | null> {
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
const where = options.where as Record<string, unknown>;
|
||||
const coreWhere = Array.isArray(where)
|
||||
? await Promise.all(
|
||||
where.map((whereItem: Record<string, unknown>) =>
|
||||
this.toCoreWhere(workspaceId, whereItem),
|
||||
),
|
||||
)
|
||||
: await this.toCoreWhere(workspaceId, where);
|
||||
|
||||
const requestedRelations =
|
||||
(options.relations as string[] | undefined)?.slice() ?? [];
|
||||
|
||||
const needsConnectedAccount =
|
||||
requestedRelations.includes('connectedAccount');
|
||||
|
||||
const coreRelations = requestedRelations.filter(
|
||||
(r) => r !== 'connectedAccount',
|
||||
);
|
||||
|
||||
const result = await this.coreRepository.findOne({
|
||||
...options,
|
||||
where: coreWhere,
|
||||
relations: coreRelations,
|
||||
} as FindOneOptions<CalendarChannelEntity>);
|
||||
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const workspaceResult =
|
||||
result as unknown as CalendarChannelWorkspaceEntity;
|
||||
|
||||
if (needsConnectedAccount) {
|
||||
const connectedAccount =
|
||||
await this.connectedAccountDataAccessService.findOne(workspaceId, {
|
||||
where: { id: result.connectedAccountId },
|
||||
});
|
||||
|
||||
if (connectedAccount) {
|
||||
workspaceResult.connectedAccount = connectedAccount;
|
||||
}
|
||||
}
|
||||
|
||||
return workspaceResult;
|
||||
}
|
||||
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
return workspaceRepository.findOne(options);
|
||||
}
|
||||
|
||||
async find(
|
||||
workspaceId: string,
|
||||
whereOrOptions?:
|
||||
| FindOptionsWhere<CalendarChannelWorkspaceEntity>
|
||||
| FindManyOptions<CalendarChannelWorkspaceEntity>,
|
||||
): Promise<CalendarChannelWorkspaceEntity[]> {
|
||||
if (
|
||||
whereOrOptions !== undefined &&
|
||||
typeof whereOrOptions === 'object' &&
|
||||
whereOrOptions !== null &&
|
||||
!Array.isArray(whereOrOptions) &&
|
||||
'where' in whereOrOptions
|
||||
) {
|
||||
const options =
|
||||
whereOrOptions as FindManyOptions<CalendarChannelWorkspaceEntity>;
|
||||
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
const { where } = options;
|
||||
|
||||
const coreWhere = Array.isArray(where)
|
||||
? await Promise.all(
|
||||
where.map((whereItem) =>
|
||||
this.toCoreWhere(
|
||||
workspaceId,
|
||||
whereItem as Record<string, unknown>,
|
||||
),
|
||||
),
|
||||
)
|
||||
: await this.toCoreWhere(
|
||||
workspaceId,
|
||||
where as Record<string, unknown>,
|
||||
);
|
||||
|
||||
return this.coreRepository.find({
|
||||
...options,
|
||||
where: coreWhere,
|
||||
} as FindManyOptions<CalendarChannelEntity>) as unknown as Promise<
|
||||
CalendarChannelWorkspaceEntity[]
|
||||
>;
|
||||
}
|
||||
|
||||
const workspaceRepository =
|
||||
await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
return workspaceRepository.find(options);
|
||||
}
|
||||
|
||||
const where = whereOrOptions as
|
||||
| FindOptionsWhere<CalendarChannelWorkspaceEntity>
|
||||
| undefined;
|
||||
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
const coreWhere = where
|
||||
? await this.toCoreWhere(workspaceId, where as Record<string, unknown>)
|
||||
: { workspaceId };
|
||||
|
||||
return this.coreRepository.find({
|
||||
where: coreWhere,
|
||||
} as FindManyOptions<CalendarChannelEntity>) as unknown as Promise<
|
||||
CalendarChannelWorkspaceEntity[]
|
||||
>;
|
||||
}
|
||||
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
return workspaceRepository.find({ where });
|
||||
}
|
||||
|
||||
async save(
|
||||
workspaceId: string,
|
||||
data: Partial<CalendarChannelWorkspaceEntity>,
|
||||
manager?: WorkspaceEntityManager,
|
||||
): Promise<void> {
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
await workspaceRepository.save(data, {}, manager);
|
||||
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
try {
|
||||
await this.coreRepository.save({
|
||||
...data,
|
||||
workspaceId,
|
||||
} as unknown as CalendarChannelEntity);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to dual-write calendarChannel to core: ${error}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async update(
|
||||
workspaceId: string,
|
||||
where: FindOptionsWhere<CalendarChannelWorkspaceEntity>,
|
||||
data: Partial<CalendarChannelWorkspaceEntity>,
|
||||
): Promise<void> {
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
await workspaceRepository.update(where, data);
|
||||
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
try {
|
||||
await this.coreRepository.update(
|
||||
{ ...where, workspaceId } as FindOptionsWhere<CalendarChannelEntity>,
|
||||
data as Record<string, unknown>,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to dual-write calendarChannel update to core: ${error}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async increment(
|
||||
workspaceId: string,
|
||||
where: FindOptionsWhere<CalendarChannelWorkspaceEntity>,
|
||||
propertyPath: string,
|
||||
value: number,
|
||||
): Promise<void> {
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
await workspaceRepository.increment(where, propertyPath, value, undefined, [
|
||||
propertyPath,
|
||||
'id',
|
||||
]);
|
||||
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
try {
|
||||
await this.coreRepository.increment(
|
||||
{
|
||||
...where,
|
||||
workspaceId,
|
||||
} as FindOptionsWhere<CalendarChannelEntity>,
|
||||
propertyPath,
|
||||
value,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to dual-write calendarChannel increment to core: ${error}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async delete(
|
||||
workspaceId: string,
|
||||
where: FindOptionsWhere<CalendarChannelWorkspaceEntity>,
|
||||
): Promise<void> {
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
await workspaceRepository.delete(where);
|
||||
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
try {
|
||||
await this.coreRepository.delete({
|
||||
...where,
|
||||
workspaceId,
|
||||
} as FindOptionsWhere<CalendarChannelEntity>);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to dual-write calendarChannel delete to core: ${error}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -1,3 +1,5 @@
|
||||
import { registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
@@ -19,6 +21,19 @@ import {
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/workspace-related-entity';
|
||||
|
||||
registerEnumType(CalendarChannelVisibility, {
|
||||
name: 'CalendarChannelVisibility',
|
||||
});
|
||||
registerEnumType(CalendarChannelSyncStatus, {
|
||||
name: 'CalendarChannelSyncStatus',
|
||||
});
|
||||
registerEnumType(CalendarChannelSyncStage, {
|
||||
name: 'CalendarChannelSyncStage',
|
||||
});
|
||||
registerEnumType(CalendarChannelContactAutoCreationPolicy, {
|
||||
name: 'CalendarChannelContactAutoCreationPolicy',
|
||||
});
|
||||
|
||||
@Entity({ name: 'calendarChannel', schema: 'core' })
|
||||
export class CalendarChannelEntity extends WorkspaceRelatedEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
|
||||
+1
-9
@@ -1,17 +1,11 @@
|
||||
import { UseGuards, UseInterceptors } from '@nestjs/common';
|
||||
import { Args, Mutation, Query } from '@nestjs/graphql';
|
||||
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import {
|
||||
FeatureFlagGuard,
|
||||
RequireFeatureFlag,
|
||||
} from 'src/engine/guards/feature-flag.guard';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { CalendarChannelMetadataService } from 'src/engine/metadata-modules/calendar-channel/calendar-channel-metadata.service';
|
||||
@@ -19,7 +13,7 @@ import { CalendarChannelDTO } from 'src/engine/metadata-modules/calendar-channel
|
||||
import { UpdateCalendarChannelInput } from 'src/engine/metadata-modules/calendar-channel/dtos/update-calendar-channel.input';
|
||||
import { CalendarChannelGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/calendar-channel/interceptors/calendar-channel-graphql-api-exception.interceptor';
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard, FeatureFlagGuard)
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
@UseInterceptors(CalendarChannelGraphqlApiExceptionInterceptor)
|
||||
@MetadataResolver(() => CalendarChannelDTO)
|
||||
export class CalendarChannelResolver {
|
||||
@@ -29,7 +23,6 @@ export class CalendarChannelResolver {
|
||||
|
||||
@Query(() => [CalendarChannelDTO])
|
||||
@UseGuards(NoPermissionGuard)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async myCalendarChannels(
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string,
|
||||
@@ -57,7 +50,6 @@ export class CalendarChannelResolver {
|
||||
|
||||
@Mutation(() => CalendarChannelDTO)
|
||||
@UseGuards(NoPermissionGuard)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async updateCalendarChannel(
|
||||
@Args('input') input: UpdateCalendarChannelInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
|
||||
+10
-8
@@ -2,25 +2,27 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { CalendarChannelDataAccessModule } from 'src/engine/metadata-modules/calendar-channel/data-access/calendar-channel-data-access.module';
|
||||
import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module';
|
||||
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
|
||||
import { ConnectedAccountMetadataService } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.service';
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { ConnectedAccountGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/connected-account/interceptors/connected-account-graphql-api-exception.interceptor';
|
||||
import { ConnectedAccountResolver } from 'src/engine/metadata-modules/connected-account/resolvers/connected-account.resolver';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { MessageChannelDataAccessModule } from 'src/engine/metadata-modules/message-channel/data-access/message-channel-data-access.module';
|
||||
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([ConnectedAccountEntity]),
|
||||
CalendarChannelDataAccessModule,
|
||||
ConnectedAccountDataAccessModule,
|
||||
TypeOrmModule.forFeature([
|
||||
ConnectedAccountEntity,
|
||||
CalendarChannelEntity,
|
||||
MessageChannelEntity,
|
||||
]),
|
||||
FeatureFlagModule,
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
MessageChannelDataAccessModule,
|
||||
PermissionsModule,
|
||||
WorkspaceEventEmitterModule,
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
],
|
||||
providers: [
|
||||
ConnectedAccountMetadataService,
|
||||
|
||||
+17
-16
@@ -11,11 +11,10 @@ import {
|
||||
} from 'src/engine/metadata-modules/connected-account/connected-account.exception';
|
||||
import { ConnectedAccountDTO } from 'src/engine/metadata-modules/connected-account/dtos/connected-account.dto';
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service';
|
||||
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
|
||||
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
|
||||
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
|
||||
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
|
||||
@@ -27,9 +26,10 @@ export class ConnectedAccountMetadataService {
|
||||
constructor(
|
||||
@InjectRepository(ConnectedAccountEntity)
|
||||
private readonly repository: Repository<ConnectedAccountEntity>,
|
||||
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
|
||||
private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService,
|
||||
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
|
||||
@InjectRepository(CalendarChannelEntity)
|
||||
private readonly calendarChannelRepository: Repository<CalendarChannelEntity>,
|
||||
@InjectRepository(MessageChannelEntity)
|
||||
private readonly messageChannelRepository: Repository<MessageChannelEntity>,
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
|
||||
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
@@ -164,15 +164,15 @@ export class ConnectedAccountMetadataService {
|
||||
const [messageChannels, calendarChannels] = await Promise.all([
|
||||
this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
async () =>
|
||||
this.messageChannelDataAccessService.find(workspaceId, {
|
||||
connectedAccountId: id,
|
||||
this.messageChannelRepository.find({
|
||||
where: { connectedAccountId: id, workspaceId },
|
||||
}),
|
||||
authContext,
|
||||
),
|
||||
this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
async () =>
|
||||
this.calendarChannelDataAccessService.find(workspaceId, {
|
||||
connectedAccountId: id,
|
||||
this.calendarChannelRepository.find({
|
||||
where: { connectedAccountId: id, workspaceId },
|
||||
}),
|
||||
authContext,
|
||||
),
|
||||
@@ -182,6 +182,13 @@ export class ConnectedAccountMetadataService {
|
||||
`WorkspaceId: ${workspaceId} Deleting connected account ${id} with ${messageChannels.length} message channel(s) and ${calendarChannels.length} calendar channel(s)`,
|
||||
);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
await this.repository.delete({
|
||||
id,
|
||||
workspaceId,
|
||||
});
|
||||
}, authContext);
|
||||
|
||||
const { flatObjectMetadataMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
@@ -234,12 +241,6 @@ export class ConnectedAccountMetadataService {
|
||||
});
|
||||
}
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
await this.connectedAccountDataAccessService.delete(workspaceId, {
|
||||
id,
|
||||
});
|
||||
}, authContext);
|
||||
|
||||
return connectedAccount;
|
||||
}
|
||||
}
|
||||
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([ConnectedAccountEntity, UserWorkspaceEntity]),
|
||||
FeatureFlagModule,
|
||||
],
|
||||
providers: [ConnectedAccountDataAccessService],
|
||||
exports: [ConnectedAccountDataAccessService],
|
||||
})
|
||||
export class ConnectedAccountDataAccessModule {}
|
||||
-352
@@ -1,352 +0,0 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
type FindOneOptions,
|
||||
type FindOptionsWhere,
|
||||
In,
|
||||
Repository,
|
||||
} from 'typeorm';
|
||||
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class ConnectedAccountDataAccessService {
|
||||
private readonly logger = new Logger(ConnectedAccountDataAccessService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(ConnectedAccountEntity)
|
||||
private readonly coreRepository: Repository<ConnectedAccountEntity>,
|
||||
@InjectRepository(UserWorkspaceEntity)
|
||||
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
) {}
|
||||
|
||||
private async isMigrated(workspaceId: string): Promise<boolean> {
|
||||
return this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
private async resolveUserWorkspaceId(
|
||||
workspaceId: string,
|
||||
workspaceMemberId: string,
|
||||
): Promise<string | null> {
|
||||
const workspaceMemberRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workspaceMember',
|
||||
);
|
||||
|
||||
const workspaceMember = await workspaceMemberRepository.findOne({
|
||||
where: { id: workspaceMemberId },
|
||||
});
|
||||
|
||||
if (!workspaceMember) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const userWorkspace = await this.userWorkspaceRepository.findOne({
|
||||
where: { userId: workspaceMember.userId, workspaceId },
|
||||
});
|
||||
|
||||
return userWorkspace?.id ?? null;
|
||||
}
|
||||
|
||||
private async toCore(
|
||||
workspaceId: string,
|
||||
data: Partial<ConnectedAccountWorkspaceEntity>,
|
||||
): Promise<Partial<ConnectedAccountEntity>> {
|
||||
const {
|
||||
handleAliases,
|
||||
lastSyncHistoryId: _lastSyncHistoryId,
|
||||
accountOwnerId,
|
||||
...rest
|
||||
} = data as Record<string, unknown>;
|
||||
|
||||
const coreData: Record<string, unknown> = { ...rest };
|
||||
|
||||
if (handleAliases !== undefined) {
|
||||
if (Array.isArray(handleAliases)) {
|
||||
coreData.handleAliases = handleAliases;
|
||||
} else if (isNonEmptyString(handleAliases)) {
|
||||
coreData.handleAliases = handleAliases
|
||||
.split(',')
|
||||
.map((alias: string) => alias.trim());
|
||||
} else {
|
||||
coreData.handleAliases = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (accountOwnerId !== undefined) {
|
||||
const userWorkspaceId = await this.resolveUserWorkspaceId(
|
||||
workspaceId,
|
||||
accountOwnerId as string,
|
||||
);
|
||||
|
||||
if (!userWorkspaceId) {
|
||||
this.logger.warn(
|
||||
`Could not resolve userWorkspaceId for workspaceMember ${accountOwnerId}`,
|
||||
);
|
||||
}
|
||||
|
||||
coreData.userWorkspaceId = userWorkspaceId;
|
||||
}
|
||||
|
||||
return coreData as Partial<ConnectedAccountEntity>;
|
||||
}
|
||||
|
||||
private async toCoreWhere(
|
||||
workspaceId: string,
|
||||
where: Record<string, unknown>,
|
||||
): Promise<FindOptionsWhere<ConnectedAccountEntity>> {
|
||||
const { accountOwnerId, ...rest } = where;
|
||||
const coreWhere: Record<string, unknown> = { ...rest, workspaceId };
|
||||
|
||||
if (accountOwnerId !== undefined) {
|
||||
const userWorkspaceId = await this.resolveUserWorkspaceId(
|
||||
workspaceId,
|
||||
accountOwnerId as string,
|
||||
);
|
||||
|
||||
if (userWorkspaceId) {
|
||||
coreWhere.userWorkspaceId = userWorkspaceId;
|
||||
} else {
|
||||
this.logger.warn(
|
||||
`toCoreWhere: could not resolve userWorkspaceId for workspaceMember ${accountOwnerId}, returning empty result`,
|
||||
);
|
||||
coreWhere.id = '00000000-0000-0000-0000-000000000000';
|
||||
}
|
||||
}
|
||||
|
||||
return coreWhere as FindOptionsWhere<ConnectedAccountEntity>;
|
||||
}
|
||||
|
||||
private async fromCoreEntities(
|
||||
workspaceId: string,
|
||||
entities: ConnectedAccountEntity[],
|
||||
): Promise<ConnectedAccountWorkspaceEntity[]> {
|
||||
if (entities.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const userWorkspaceIds = entities
|
||||
.map((entity) => entity.userWorkspaceId)
|
||||
.filter(isDefined);
|
||||
|
||||
const userWorkspaces =
|
||||
userWorkspaceIds.length > 0
|
||||
? await this.userWorkspaceRepository.find({
|
||||
where: { id: In(userWorkspaceIds) },
|
||||
select: ['id', 'userId'],
|
||||
})
|
||||
: [];
|
||||
|
||||
const userIdByUserWorkspaceId = new Map(
|
||||
userWorkspaces.map((userWorkspace) => [
|
||||
userWorkspace.id,
|
||||
userWorkspace.userId,
|
||||
]),
|
||||
);
|
||||
|
||||
const uniqueUserIds = [
|
||||
...new Set(userWorkspaces.map((userWorkspace) => userWorkspace.userId)),
|
||||
];
|
||||
|
||||
const workspaceMemberRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workspaceMember',
|
||||
);
|
||||
|
||||
const workspaceMembers =
|
||||
uniqueUserIds.length > 0
|
||||
? await workspaceMemberRepository.find({
|
||||
where: { userId: In(uniqueUserIds) },
|
||||
})
|
||||
: [];
|
||||
|
||||
const workspaceMemberIdByUserId = new Map(
|
||||
workspaceMembers.map((workspaceMember) => [
|
||||
workspaceMember.userId,
|
||||
workspaceMember.id,
|
||||
]),
|
||||
);
|
||||
|
||||
return entities.map((entity) => {
|
||||
const userId = entity.userWorkspaceId
|
||||
? userIdByUserWorkspaceId.get(entity.userWorkspaceId)
|
||||
: undefined;
|
||||
|
||||
const accountOwnerId = userId
|
||||
? workspaceMemberIdByUserId.get(userId)
|
||||
: undefined;
|
||||
|
||||
const handleAliases = Array.isArray(entity.handleAliases)
|
||||
? entity.handleAliases.join(',')
|
||||
: (entity.handleAliases ?? '');
|
||||
|
||||
return {
|
||||
...entity,
|
||||
handleAliases,
|
||||
accountOwnerId: accountOwnerId ?? null,
|
||||
} as unknown as ConnectedAccountWorkspaceEntity;
|
||||
});
|
||||
}
|
||||
|
||||
async getWorkspaceRepository(workspaceId: string) {
|
||||
return this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'connectedAccount',
|
||||
);
|
||||
}
|
||||
|
||||
async findOne(
|
||||
workspaceId: string,
|
||||
options: FindOneOptions<ConnectedAccountWorkspaceEntity>,
|
||||
): Promise<ConnectedAccountWorkspaceEntity | null> {
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
const where = options.where as Record<string, unknown>;
|
||||
const coreWhere = Array.isArray(where)
|
||||
? await Promise.all(
|
||||
where.map((whereItem: Record<string, unknown>) =>
|
||||
this.toCoreWhere(workspaceId, whereItem),
|
||||
),
|
||||
)
|
||||
: await this.toCoreWhere(workspaceId, where);
|
||||
|
||||
const coreResult = await this.coreRepository.findOne({
|
||||
...options,
|
||||
where: coreWhere,
|
||||
} as FindOneOptions<ConnectedAccountEntity>);
|
||||
|
||||
if (!coreResult) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [transformed] = await this.fromCoreEntities(workspaceId, [
|
||||
coreResult,
|
||||
]);
|
||||
|
||||
return transformed ?? null;
|
||||
}
|
||||
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
return workspaceRepository.findOne(options);
|
||||
}
|
||||
|
||||
async find(
|
||||
workspaceId: string,
|
||||
where?: FindOptionsWhere<ConnectedAccountWorkspaceEntity>,
|
||||
): Promise<ConnectedAccountWorkspaceEntity[]> {
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
const coreWhere = where
|
||||
? await this.toCoreWhere(workspaceId, where as Record<string, unknown>)
|
||||
: { workspaceId };
|
||||
|
||||
const coreResults = await this.coreRepository.find({
|
||||
where: coreWhere,
|
||||
});
|
||||
|
||||
return this.fromCoreEntities(workspaceId, coreResults);
|
||||
}
|
||||
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
return workspaceRepository.find({ where });
|
||||
}
|
||||
|
||||
async save(
|
||||
workspaceId: string,
|
||||
data: Partial<ConnectedAccountWorkspaceEntity>,
|
||||
manager?: WorkspaceEntityManager,
|
||||
): Promise<void> {
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
await workspaceRepository.save(data, {}, manager);
|
||||
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
try {
|
||||
const coreData = await this.toCore(workspaceId, data);
|
||||
|
||||
await this.coreRepository.save({
|
||||
...coreData,
|
||||
workspaceId,
|
||||
} as ConnectedAccountEntity);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to dual-write connectedAccount to core: ${error}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async update(
|
||||
workspaceId: string,
|
||||
where: FindOptionsWhere<ConnectedAccountWorkspaceEntity>,
|
||||
data: Partial<ConnectedAccountWorkspaceEntity>,
|
||||
): Promise<void> {
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
await workspaceRepository.update(where, data);
|
||||
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
try {
|
||||
const coreData = await this.toCore(workspaceId, data);
|
||||
const coreWhere = await this.toCoreWhere(
|
||||
workspaceId,
|
||||
where as Record<string, unknown>,
|
||||
);
|
||||
|
||||
await this.coreRepository.update(
|
||||
coreWhere,
|
||||
coreData as Record<string, unknown>,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to dual-write connectedAccount update to core: ${error}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async delete(
|
||||
workspaceId: string,
|
||||
where: FindOptionsWhere<ConnectedAccountWorkspaceEntity>,
|
||||
): Promise<void> {
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
await workspaceRepository.delete(where);
|
||||
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
try {
|
||||
const coreWhere = await this.toCoreWhere(
|
||||
workspaceId,
|
||||
where as Record<string, unknown>,
|
||||
);
|
||||
|
||||
await this.coreRepository.delete(coreWhere);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to dual-write connectedAccount delete to core: ${error}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-2
@@ -8,6 +8,9 @@ import {
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import { type ConnectedAccountProvider } from 'twenty-shared/types';
|
||||
|
||||
import { type ImapSmtpCaldavParams } from 'src/engine/core-modules/imap-smtp-caldav-connection/types/imap-smtp-caldav-connection.type';
|
||||
import { type CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
|
||||
import { type MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/workspace-related-entity';
|
||||
@@ -21,7 +24,7 @@ export class ConnectedAccountEntity extends WorkspaceRelatedEntity {
|
||||
handle: string;
|
||||
|
||||
@Column({ type: 'varchar', nullable: false })
|
||||
provider: string;
|
||||
provider: ConnectedAccountProvider;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
accessToken: string | null;
|
||||
@@ -42,7 +45,7 @@ export class ConnectedAccountEntity extends WorkspaceRelatedEntity {
|
||||
scopes: string[] | null;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
connectionParameters: Record<string, unknown> | null;
|
||||
connectionParameters: ImapSmtpCaldavParams | null;
|
||||
|
||||
@Column({ type: 'timestamptz', nullable: true })
|
||||
lastSignedInAt: Date | null;
|
||||
|
||||
+1
-9
@@ -2,17 +2,12 @@ import { UseGuards, UseInterceptors } from '@nestjs/common';
|
||||
import { Args, Mutation, Query } from '@nestjs/graphql';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import {
|
||||
FeatureFlagGuard,
|
||||
RequireFeatureFlag,
|
||||
} from 'src/engine/guards/feature-flag.guard';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
@@ -20,7 +15,7 @@ import { ConnectedAccountMetadataService } from 'src/engine/metadata-modules/con
|
||||
import { ConnectedAccountDTO } from 'src/engine/metadata-modules/connected-account/dtos/connected-account.dto';
|
||||
import { ConnectedAccountGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/connected-account/interceptors/connected-account-graphql-api-exception.interceptor';
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard, FeatureFlagGuard)
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
@UseInterceptors(ConnectedAccountGraphqlApiExceptionInterceptor)
|
||||
@MetadataResolver(() => ConnectedAccountDTO)
|
||||
export class ConnectedAccountResolver {
|
||||
@@ -30,7 +25,6 @@ export class ConnectedAccountResolver {
|
||||
|
||||
@Query(() => [ConnectedAccountDTO])
|
||||
@UseGuards(NoPermissionGuard)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async myConnectedAccounts(
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string,
|
||||
@@ -43,7 +37,6 @@ export class ConnectedAccountResolver {
|
||||
|
||||
@Query(() => [ConnectedAccountDTO])
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async connectedAccounts(
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<ConnectedAccountDTO[]> {
|
||||
@@ -52,7 +45,6 @@ export class ConnectedAccountResolver {
|
||||
|
||||
@Mutation(() => ConnectedAccountDTO)
|
||||
@UseGuards(NoPermissionGuard)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async deleteConnectedAccount(
|
||||
@Args('id', { type: () => UUIDScalarType }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module';
|
||||
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
|
||||
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { MessageFolderDataAccessModule } from 'src/engine/metadata-modules/message-folder/data-access/message-folder-data-access.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([MessageChannelEntity]),
|
||||
FeatureFlagModule,
|
||||
ConnectedAccountDataAccessModule,
|
||||
MessageFolderDataAccessModule,
|
||||
],
|
||||
providers: [MessageChannelDataAccessService],
|
||||
exports: [MessageChannelDataAccessService],
|
||||
})
|
||||
export class MessageChannelDataAccessModule {}
|
||||
-327
@@ -1,327 +0,0 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
import {
|
||||
type FindManyOptions,
|
||||
type FindOneOptions,
|
||||
type FindOptionsWhere,
|
||||
Repository,
|
||||
} from 'typeorm';
|
||||
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
|
||||
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { MessageFolderDataAccessService } from 'src/engine/metadata-modules/message-folder/data-access/services/message-folder-data-access.service';
|
||||
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class MessageChannelDataAccessService {
|
||||
private readonly logger = new Logger(MessageChannelDataAccessService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(MessageChannelEntity)
|
||||
private readonly coreRepository: Repository<MessageChannelEntity>,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
|
||||
private readonly messageFolderDataAccessService: MessageFolderDataAccessService,
|
||||
) {}
|
||||
|
||||
private async isMigrated(workspaceId: string): Promise<boolean> {
|
||||
return this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
private async toCoreWhere(
|
||||
workspaceId: string,
|
||||
where: Record<string, unknown>,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const coreWhere: Record<string, unknown> = { ...where, workspaceId };
|
||||
|
||||
if (
|
||||
coreWhere.connectedAccount &&
|
||||
typeof coreWhere.connectedAccount === 'object'
|
||||
) {
|
||||
const connectedAccountWhere = {
|
||||
...(coreWhere.connectedAccount as Record<string, unknown>),
|
||||
};
|
||||
|
||||
if ('accountOwnerId' in connectedAccountWhere) {
|
||||
const { accountOwnerId, ...restConnectedAccount } =
|
||||
connectedAccountWhere;
|
||||
|
||||
const resolvedConnectedAccounts =
|
||||
await this.connectedAccountDataAccessService.find(workspaceId, {
|
||||
accountOwnerId,
|
||||
} as never);
|
||||
|
||||
if (resolvedConnectedAccounts.length > 0) {
|
||||
coreWhere.connectedAccountId = resolvedConnectedAccounts[0].id;
|
||||
} else {
|
||||
coreWhere.connectedAccountId = '00000000-0000-0000-0000-000000000000';
|
||||
}
|
||||
|
||||
if (Object.keys(restConnectedAccount).length > 0) {
|
||||
coreWhere.connectedAccount = restConnectedAccount;
|
||||
} else {
|
||||
delete coreWhere.connectedAccount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return coreWhere;
|
||||
}
|
||||
|
||||
async getWorkspaceRepository(workspaceId: string) {
|
||||
return this.globalWorkspaceOrmManager.getRepository<MessageChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'messageChannel',
|
||||
);
|
||||
}
|
||||
|
||||
async findOne(
|
||||
workspaceId: string,
|
||||
options: FindOneOptions<MessageChannelWorkspaceEntity>,
|
||||
): Promise<MessageChannelWorkspaceEntity | null> {
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
const where = options.where as Record<string, unknown>;
|
||||
const coreWhere = Array.isArray(where)
|
||||
? await Promise.all(
|
||||
where.map((whereItem: Record<string, unknown>) =>
|
||||
this.toCoreWhere(workspaceId, whereItem),
|
||||
),
|
||||
)
|
||||
: await this.toCoreWhere(workspaceId, where);
|
||||
|
||||
const requestedRelations =
|
||||
(options.relations as string[] | undefined)?.slice() ?? [];
|
||||
|
||||
const needsConnectedAccount =
|
||||
requestedRelations.includes('connectedAccount');
|
||||
|
||||
const needsMessageFolders = requestedRelations.includes('messageFolders');
|
||||
|
||||
const coreRelations = requestedRelations.filter(
|
||||
(r) => r !== 'connectedAccount' && r !== 'messageFolders',
|
||||
);
|
||||
|
||||
const result = await this.coreRepository.findOne({
|
||||
...options,
|
||||
where: coreWhere,
|
||||
relations: coreRelations,
|
||||
} as FindOneOptions<MessageChannelEntity>);
|
||||
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const workspaceResult =
|
||||
result as unknown as MessageChannelWorkspaceEntity;
|
||||
|
||||
if (needsConnectedAccount) {
|
||||
const connectedAccount =
|
||||
await this.connectedAccountDataAccessService.findOne(workspaceId, {
|
||||
where: { id: result.connectedAccountId },
|
||||
});
|
||||
|
||||
if (connectedAccount) {
|
||||
workspaceResult.connectedAccount = connectedAccount;
|
||||
}
|
||||
}
|
||||
|
||||
if (needsMessageFolders) {
|
||||
const messageFolders = await this.messageFolderDataAccessService.find(
|
||||
workspaceId,
|
||||
{
|
||||
messageChannelId: result.id,
|
||||
},
|
||||
);
|
||||
|
||||
workspaceResult.messageFolders =
|
||||
messageFolders as unknown as MessageChannelWorkspaceEntity['messageFolders'];
|
||||
}
|
||||
|
||||
return workspaceResult;
|
||||
}
|
||||
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
return workspaceRepository.findOne(options);
|
||||
}
|
||||
|
||||
async find(
|
||||
workspaceId: string,
|
||||
where?: FindOptionsWhere<MessageChannelWorkspaceEntity>,
|
||||
): Promise<MessageChannelWorkspaceEntity[]> {
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
return this.coreRepository.find({
|
||||
where: {
|
||||
...(where as Record<string, unknown>),
|
||||
workspaceId,
|
||||
} as FindOptionsWhere<MessageChannelEntity>,
|
||||
}) as unknown as Promise<MessageChannelWorkspaceEntity[]>;
|
||||
}
|
||||
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
return workspaceRepository.find({ where });
|
||||
}
|
||||
|
||||
async findMany(
|
||||
workspaceId: string,
|
||||
options: FindManyOptions<MessageChannelWorkspaceEntity>,
|
||||
): Promise<MessageChannelWorkspaceEntity[]> {
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
const baseWhere = options.where;
|
||||
|
||||
if (!baseWhere) {
|
||||
return this.coreRepository.find({
|
||||
...options,
|
||||
where: { workspaceId },
|
||||
} as FindManyOptions<MessageChannelEntity>) as unknown as Promise<
|
||||
MessageChannelWorkspaceEntity[]
|
||||
>;
|
||||
}
|
||||
|
||||
if (Array.isArray(baseWhere)) {
|
||||
const coreWhereArray = await Promise.all(
|
||||
baseWhere.map((whereItem) =>
|
||||
this.toCoreWhere(workspaceId, whereItem as Record<string, unknown>),
|
||||
),
|
||||
);
|
||||
|
||||
return this.coreRepository.find({
|
||||
...options,
|
||||
where: coreWhereArray,
|
||||
} as FindManyOptions<MessageChannelEntity>) as unknown as Promise<
|
||||
MessageChannelWorkspaceEntity[]
|
||||
>;
|
||||
}
|
||||
|
||||
const coreWhere = await this.toCoreWhere(
|
||||
workspaceId,
|
||||
baseWhere as Record<string, unknown>,
|
||||
);
|
||||
|
||||
return this.coreRepository.find({
|
||||
...options,
|
||||
where: coreWhere,
|
||||
} as FindManyOptions<MessageChannelEntity>) as unknown as Promise<
|
||||
MessageChannelWorkspaceEntity[]
|
||||
>;
|
||||
}
|
||||
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
return workspaceRepository.find(options);
|
||||
}
|
||||
|
||||
async save(
|
||||
workspaceId: string,
|
||||
data: Partial<MessageChannelWorkspaceEntity>,
|
||||
manager?: WorkspaceEntityManager,
|
||||
): Promise<void> {
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
await workspaceRepository.save(data, {}, manager);
|
||||
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
try {
|
||||
await this.coreRepository.save({
|
||||
...data,
|
||||
workspaceId,
|
||||
} as unknown as MessageChannelEntity);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to dual-write messageChannel to core: ${error}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async update(
|
||||
workspaceId: string,
|
||||
where: FindOptionsWhere<MessageChannelWorkspaceEntity>,
|
||||
data: Partial<MessageChannelWorkspaceEntity>,
|
||||
manager?: WorkspaceEntityManager,
|
||||
): Promise<void> {
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
await workspaceRepository.update(where, data, manager);
|
||||
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
try {
|
||||
await this.coreRepository.update(
|
||||
{ ...where, workspaceId } as FindOptionsWhere<MessageChannelEntity>,
|
||||
data as never,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to dual-write messageChannel update to core: ${error}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async increment(
|
||||
workspaceId: string,
|
||||
where: FindOptionsWhere<MessageChannelWorkspaceEntity>,
|
||||
propertyPath: string,
|
||||
value: number,
|
||||
): Promise<void> {
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
await workspaceRepository.increment(where, propertyPath, value, undefined, [
|
||||
propertyPath,
|
||||
'id',
|
||||
]);
|
||||
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
try {
|
||||
await this.coreRepository.increment(
|
||||
{
|
||||
...where,
|
||||
workspaceId,
|
||||
} as FindOptionsWhere<MessageChannelEntity>,
|
||||
propertyPath,
|
||||
value,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to dual-write messageChannel increment to core: ${error}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async delete(
|
||||
workspaceId: string,
|
||||
where: FindOptionsWhere<MessageChannelWorkspaceEntity>,
|
||||
): Promise<void> {
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
await workspaceRepository.delete(where);
|
||||
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
try {
|
||||
await this.coreRepository.delete({
|
||||
...where,
|
||||
workspaceId,
|
||||
} as FindOptionsWhere<MessageChannelEntity>);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to dual-write messageChannel delete to core: ${error}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
@@ -1,3 +1,5 @@
|
||||
import { registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
@@ -24,6 +26,26 @@ import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-ac
|
||||
import { type MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
|
||||
import { WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/workspace-related-entity';
|
||||
|
||||
registerEnumType(MessageChannelVisibility, {
|
||||
name: 'MessageChannelVisibility',
|
||||
});
|
||||
registerEnumType(MessageChannelSyncStatus, {
|
||||
name: 'MessageChannelSyncStatus',
|
||||
});
|
||||
registerEnumType(MessageChannelSyncStage, {
|
||||
name: 'MessageChannelSyncStage',
|
||||
});
|
||||
registerEnumType(MessageChannelType, { name: 'MessageChannelType' });
|
||||
registerEnumType(MessageChannelContactAutoCreationPolicy, {
|
||||
name: 'MessageChannelContactAutoCreationPolicy',
|
||||
});
|
||||
registerEnumType(MessageFolderImportPolicy, {
|
||||
name: 'MessageFolderImportPolicy',
|
||||
});
|
||||
registerEnumType(MessageChannelPendingGroupEmailsAction, {
|
||||
name: 'MessageChannelPendingGroupEmailsAction',
|
||||
});
|
||||
|
||||
@Entity({ name: 'messageChannel', schema: 'core' })
|
||||
export class MessageChannelEntity extends WorkspaceRelatedEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
|
||||
+2
-5
@@ -1,23 +1,20 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { ConnectedAccountMetadataModule } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.module';
|
||||
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { MessageChannelGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/message-channel/interceptors/message-channel-graphql-api-exception.interceptor';
|
||||
import { MessageChannelMetadataService } from 'src/engine/metadata-modules/message-channel/message-channel-metadata.service';
|
||||
import { MessageChannelResolver } from 'src/engine/metadata-modules/message-channel/resolvers/message-channel.resolver';
|
||||
import { MessageFolderDataAccessModule } from 'src/engine/metadata-modules/message-folder/data-access/message-folder-data-access.module';
|
||||
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { MessagingImportManagerModule } from 'src/modules/messaging/message-import-manager/messaging-import-manager.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([MessageChannelEntity]),
|
||||
TypeOrmModule.forFeature([MessageChannelEntity, MessageFolderEntity]),
|
||||
PermissionsModule,
|
||||
FeatureFlagModule,
|
||||
ConnectedAccountMetadataModule,
|
||||
MessageFolderDataAccessModule,
|
||||
MessagingImportManagerModule,
|
||||
],
|
||||
providers: [
|
||||
|
||||
+19
-21
@@ -1,18 +1,22 @@
|
||||
import { UseGuards, UseInterceptors } from '@nestjs/common';
|
||||
import { Args, Mutation, Query } from '@nestjs/graphql';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
|
||||
import { Not, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
MessageChannelPendingGroupEmailsAction,
|
||||
MessageChannelSyncStage,
|
||||
MessageFolderPendingSyncAction,
|
||||
} from 'twenty-shared/types';
|
||||
import { type MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import {
|
||||
FeatureFlagGuard,
|
||||
RequireFeatureFlag,
|
||||
} from 'src/engine/guards/feature-flag.guard';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { MessageChannelDTO } from 'src/engine/metadata-modules/message-channel/dtos/message-channel.dto';
|
||||
@@ -23,29 +27,22 @@ import {
|
||||
} from 'src/engine/metadata-modules/message-channel/message-channel.exception';
|
||||
import { MessageChannelGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/message-channel/interceptors/message-channel-graphql-api-exception.interceptor';
|
||||
import { MessageChannelMetadataService } from 'src/engine/metadata-modules/message-channel/message-channel-metadata.service';
|
||||
import {
|
||||
MessageChannelPendingGroupEmailsAction,
|
||||
MessageChannelSyncStage,
|
||||
type MessageChannelWorkspaceEntity,
|
||||
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import { MessageFolderPendingSyncAction } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
|
||||
import { MessageFolderDataAccessService } from 'src/engine/metadata-modules/message-folder/data-access/services/message-folder-data-access.service';
|
||||
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
|
||||
import { MessagingProcessGroupEmailActionsService } from 'src/modules/messaging/message-import-manager/services/messaging-process-group-email-actions.service';
|
||||
import { Not } from 'typeorm';
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard, FeatureFlagGuard)
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
@UseInterceptors(MessageChannelGraphqlApiExceptionInterceptor)
|
||||
@MetadataResolver(() => MessageChannelDTO)
|
||||
export class MessageChannelResolver {
|
||||
constructor(
|
||||
private readonly messageChannelMetadataService: MessageChannelMetadataService,
|
||||
private readonly messageFolderDataAccessService: MessageFolderDataAccessService,
|
||||
@InjectRepository(MessageFolderEntity)
|
||||
private readonly messageFolderRepository: Repository<MessageFolderEntity>,
|
||||
private readonly messagingProcessGroupEmailActionsService: MessagingProcessGroupEmailActionsService,
|
||||
) {}
|
||||
|
||||
@Query(() => [MessageChannelDTO])
|
||||
@UseGuards(NoPermissionGuard)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async myMessageChannels(
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string,
|
||||
@@ -73,7 +70,6 @@ export class MessageChannelResolver {
|
||||
|
||||
@Mutation(() => MessageChannelDTO)
|
||||
@UseGuards(NoPermissionGuard)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async updateMessageChannel(
|
||||
@Args('input') input: UpdateMessageChannelInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@@ -90,11 +86,13 @@ export class MessageChannelResolver {
|
||||
messageChannel.syncStage ===
|
||||
MessageChannelSyncStage.MESSAGE_LIST_FETCH_ONGOING;
|
||||
|
||||
const foldersWithPendingAction =
|
||||
await this.messageFolderDataAccessService.find(workspace.id, {
|
||||
const foldersWithPendingAction = await this.messageFolderRepository.find({
|
||||
where: {
|
||||
messageChannelId: messageChannel.id,
|
||||
pendingSyncAction: Not(MessageFolderPendingSyncAction.NONE),
|
||||
});
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
});
|
||||
|
||||
const hasPendingGroupEmailsAction =
|
||||
messageChannel.pendingGroupEmailsAction !==
|
||||
@@ -118,7 +116,7 @@ export class MessageChannelResolver {
|
||||
) {
|
||||
// Service expects WorkspaceEntity type but only reads .id
|
||||
await this.messagingProcessGroupEmailActionsService.markMessageChannelAsPendingGroupEmailsAction(
|
||||
messageChannel as unknown as MessageChannelWorkspaceEntity,
|
||||
messageChannel as unknown as MessageChannelEntity,
|
||||
workspace.id,
|
||||
input.update.excludeGroupEmails
|
||||
? MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_DELETION
|
||||
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { MessageFolderDataAccessService } from 'src/engine/metadata-modules/message-folder/data-access/services/message-folder-data-access.service';
|
||||
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([MessageFolderEntity]), FeatureFlagModule],
|
||||
providers: [MessageFolderDataAccessService],
|
||||
exports: [MessageFolderDataAccessService],
|
||||
})
|
||||
export class MessageFolderDataAccessModule {}
|
||||
-216
@@ -1,216 +0,0 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { validate as uuidValidate } from 'uuid';
|
||||
import {
|
||||
type FindOneOptions,
|
||||
type FindOptionsWhere,
|
||||
Repository,
|
||||
} from 'typeorm';
|
||||
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
|
||||
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { type MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class MessageFolderDataAccessService {
|
||||
private readonly logger = new Logger(MessageFolderDataAccessService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(MessageFolderEntity)
|
||||
private readonly coreRepository: Repository<MessageFolderEntity>,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
) {}
|
||||
|
||||
private async isMigrated(workspaceId: string): Promise<boolean> {
|
||||
return this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
private async resolveParentFolderIdForCore(
|
||||
workspaceId: string,
|
||||
parentFolderId: string | null,
|
||||
messageChannelId: string | undefined,
|
||||
): Promise<string | null> {
|
||||
if (!isNonEmptyString(parentFolderId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (uuidValidate(parentFolderId)) {
|
||||
return parentFolderId;
|
||||
}
|
||||
|
||||
if (!isDefined(messageChannelId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parentFolder = await this.coreRepository.findOne({
|
||||
where: {
|
||||
workspaceId,
|
||||
messageChannelId,
|
||||
externalId: parentFolderId,
|
||||
},
|
||||
select: ['id'],
|
||||
});
|
||||
|
||||
return parentFolder?.id ?? null;
|
||||
}
|
||||
|
||||
private async toCore(
|
||||
workspaceId: string,
|
||||
data: Partial<MessageFolderWorkspaceEntity>,
|
||||
messageChannelId?: string,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const coreData: Record<string, unknown> = { ...data, workspaceId };
|
||||
|
||||
const channelId = (coreData.messageChannelId as string) ?? messageChannelId;
|
||||
|
||||
if ('parentFolderId' in coreData) {
|
||||
coreData.parentFolderId = await this.resolveParentFolderIdForCore(
|
||||
workspaceId,
|
||||
coreData.parentFolderId as string | null,
|
||||
channelId,
|
||||
);
|
||||
}
|
||||
|
||||
return coreData;
|
||||
}
|
||||
|
||||
async getWorkspaceRepository(workspaceId: string) {
|
||||
return this.globalWorkspaceOrmManager.getRepository<MessageFolderWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'messageFolder',
|
||||
);
|
||||
}
|
||||
|
||||
async findOne(
|
||||
workspaceId: string,
|
||||
options: FindOneOptions<MessageFolderWorkspaceEntity>,
|
||||
): Promise<MessageFolderWorkspaceEntity | null> {
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
const where = options.where as Record<string, unknown>;
|
||||
const coreWhere = Array.isArray(where)
|
||||
? where.map((whereItem) => ({
|
||||
...(whereItem as Record<string, unknown>),
|
||||
workspaceId,
|
||||
}))
|
||||
: {
|
||||
...(where as Record<string, unknown>),
|
||||
workspaceId,
|
||||
};
|
||||
|
||||
return this.coreRepository.findOne({
|
||||
...options,
|
||||
where: coreWhere,
|
||||
} as FindOneOptions<MessageFolderEntity>) as unknown as Promise<MessageFolderWorkspaceEntity | null>;
|
||||
}
|
||||
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
return workspaceRepository.findOne(options);
|
||||
}
|
||||
|
||||
async find(
|
||||
workspaceId: string,
|
||||
where?: FindOptionsWhere<MessageFolderWorkspaceEntity>,
|
||||
): Promise<MessageFolderWorkspaceEntity[]> {
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
return this.coreRepository.find({
|
||||
where: {
|
||||
...(where as Record<string, unknown>),
|
||||
workspaceId,
|
||||
} as FindOptionsWhere<MessageFolderEntity>,
|
||||
}) as unknown as Promise<MessageFolderWorkspaceEntity[]>;
|
||||
}
|
||||
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
return workspaceRepository.find({ where });
|
||||
}
|
||||
|
||||
async save(
|
||||
workspaceId: string,
|
||||
data: Partial<MessageFolderWorkspaceEntity>,
|
||||
): Promise<void> {
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
const savedData = await workspaceRepository.save(data);
|
||||
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
try {
|
||||
const coreData = await this.toCore(workspaceId, savedData);
|
||||
|
||||
await this.coreRepository.save(
|
||||
coreData as unknown as MessageFolderEntity,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to dual-write messageFolder to core: ${error}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async update(
|
||||
workspaceId: string,
|
||||
where: FindOptionsWhere<MessageFolderWorkspaceEntity>,
|
||||
data: Partial<MessageFolderWorkspaceEntity>,
|
||||
manager?: WorkspaceEntityManager,
|
||||
): Promise<void> {
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
await workspaceRepository.update(where, data, manager);
|
||||
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
try {
|
||||
const coreData = await this.toCore(
|
||||
workspaceId,
|
||||
data,
|
||||
(where as Record<string, unknown>).messageChannelId as string,
|
||||
);
|
||||
|
||||
await this.coreRepository.update(
|
||||
{ ...where, workspaceId } as FindOptionsWhere<MessageFolderEntity>,
|
||||
coreData,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to dual-write messageFolder update to core: ${error}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async delete(
|
||||
workspaceId: string,
|
||||
where: FindOptionsWhere<MessageFolderWorkspaceEntity>,
|
||||
): Promise<void> {
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
await workspaceRepository.delete(where);
|
||||
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
try {
|
||||
await this.coreRepository.delete({
|
||||
...where,
|
||||
workspaceId,
|
||||
} as FindOptionsWhere<MessageFolderEntity>);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to dual-write messageFolder delete to core: ${error}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -49,8 +49,8 @@ export class CreateMessageFolderInput {
|
||||
@Field(() => UUIDScalarType)
|
||||
messageChannelId: string;
|
||||
|
||||
@IsUUID()
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
@Field(() => String, { nullable: true })
|
||||
parentFolderId?: string;
|
||||
}
|
||||
|
||||
+2
-2
@@ -36,9 +36,9 @@ export class MessageFolderDTO {
|
||||
@Field()
|
||||
isSynced: boolean;
|
||||
|
||||
@IsUUID()
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
@Field(() => String, { nullable: true })
|
||||
parentFolderId: string | null;
|
||||
|
||||
@IsString()
|
||||
|
||||
+1
-10
@@ -1,17 +1,11 @@
|
||||
import { UseGuards, UseInterceptors } from '@nestjs/common';
|
||||
import { Args, Mutation, Query } from '@nestjs/graphql';
|
||||
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import {
|
||||
FeatureFlagGuard,
|
||||
RequireFeatureFlag,
|
||||
} from 'src/engine/guards/feature-flag.guard';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { MessageFolderDTO } from 'src/engine/metadata-modules/message-folder/dtos/message-folder.dto';
|
||||
@@ -22,7 +16,7 @@ import {
|
||||
import { MessageFolderGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/message-folder/interceptors/message-folder-graphql-api-exception.interceptor';
|
||||
import { MessageFolderMetadataService } from 'src/engine/metadata-modules/message-folder/message-folder-metadata.service';
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard, FeatureFlagGuard)
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
@UseInterceptors(MessageFolderGraphqlApiExceptionInterceptor)
|
||||
@MetadataResolver(() => MessageFolderDTO)
|
||||
export class MessageFolderResolver {
|
||||
@@ -32,7 +26,6 @@ export class MessageFolderResolver {
|
||||
|
||||
@Query(() => [MessageFolderDTO])
|
||||
@UseGuards(NoPermissionGuard)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async myMessageFolders(
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string,
|
||||
@@ -58,7 +51,6 @@ export class MessageFolderResolver {
|
||||
|
||||
@Mutation(() => MessageFolderDTO)
|
||||
@UseGuards(NoPermissionGuard)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async updateMessageFolder(
|
||||
@Args('input') input: UpdateMessageFolderInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@@ -79,7 +71,6 @@ export class MessageFolderResolver {
|
||||
|
||||
@Mutation(() => [MessageFolderDTO])
|
||||
@UseGuards(NoPermissionGuard)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async updateMessageFolders(
|
||||
@Args('input') input: UpdateMessageFoldersInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
ApplicationException,
|
||||
ApplicationExceptionCode,
|
||||
} from 'src/engine/core-modules/application/application.exception';
|
||||
import { isApiKeyAuthContext } from 'src/engine/core-modules/auth/guards/is-api-key-auth-context.guard';
|
||||
import { TOOL_PERMISSION_FLAGS } from 'src/engine/metadata-modules/permissions/constants/tool-permission-flags';
|
||||
import {
|
||||
PermissionsException,
|
||||
|
||||
-1
@@ -243,7 +243,6 @@ describe('WorkspaceEntityManager', () => {
|
||||
IS_USAGE_ANALYTICS_ENABLED: false,
|
||||
IS_RICH_TEXT_V1_MIGRATED: false,
|
||||
IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED: false,
|
||||
IS_CONNECTED_ACCOUNT_MIGRATED: false,
|
||||
IS_RECORD_TABLE_WIDGET_ENABLED: false,
|
||||
IS_DATASOURCE_MIGRATED: false,
|
||||
IS_COMMAND_MENU_ITEM_ENABLED: false,
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ export const applyRowLevelPermissionPredicates = <T extends ObjectLiteral>({
|
||||
objectMetadata,
|
||||
internalContext,
|
||||
authContext,
|
||||
featureFlagMap,
|
||||
featureFlagMap: _featureFlagMap,
|
||||
}: ApplyRowLevelPermissionPredicatesArgs<T>): void => {
|
||||
const userWorkspaceId = isUserAuthContext(authContext)
|
||||
? authContext.userWorkspaceId
|
||||
|
||||
+197
-34
@@ -24,6 +24,84 @@ type SeedMetadataEntitiesArgs = {
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
// YC workspace needs its own entity IDs since core tables have a single PK
|
||||
const YC_CONNECTED_ACCOUNT_IDS = {
|
||||
TIM: '30303030-9ac0-4390-9a1a-ab4d2c4e1bb7',
|
||||
JONY: '30303030-0cc8-4d60-a3a4-803245698908',
|
||||
PHIL: '30303030-cafc-4323-908d-e5b42ad69fdf',
|
||||
JANE: '30303030-b5c7-46f0-bf5c-3f4e4b3f7c1a',
|
||||
JANE_DELETABLE: '30303030-d1e5-4a8f-9c3b-7f6d5e4c3b2a',
|
||||
};
|
||||
|
||||
const YC_MESSAGE_CHANNEL_IDS = {
|
||||
TIM: '30303030-9b80-4c2c-a597-383db48de1d6',
|
||||
JONY: '30303030-5ffe-4b32-814a-983d5e4911cd',
|
||||
PHIL: '30303030-e2f1-49b5-85d2-5d3a3386990c',
|
||||
JANE: '30303030-8c4d-4e71-a672-2e6a8c9f1b3d',
|
||||
SUPPORT: '30303030-e2f1-49b5-85d2-5d3a3386990d',
|
||||
SALES: '30303030-e2f1-49b5-85d2-5d3a3386990e',
|
||||
};
|
||||
|
||||
const YC_CALENDAR_CHANNEL_IDS = {
|
||||
TIM: '30303030-a40f-4faf-bb9f-c6f9945b8203',
|
||||
JONY: '30303030-a40f-4faf-bb9f-c6f9945b8204',
|
||||
PHIL: '30303030-a40f-4faf-bb9f-c6f9945b8205',
|
||||
JANE: '30303030-a40f-4faf-bb9f-c6f9945b8208',
|
||||
COMPANY_MAIN: '30303030-a40f-4faf-bb9f-c6f9945b8206',
|
||||
TEAM_CALENDAR: '30303030-a40f-4faf-bb9f-c6f9945b8207',
|
||||
};
|
||||
|
||||
const YC_MESSAGE_FOLDER_IDS = {
|
||||
TIM_INBOX: '30303030-1234-4567-8901-abcdef012345',
|
||||
TIM_SENT: '30303030-1234-4567-8901-abcdef012349',
|
||||
JONY_INBOX: '30303030-1234-4567-8901-abcdef012346',
|
||||
JANE_INBOX: '30303030-1234-4567-8901-abcdef012347',
|
||||
JANE_SENT: '30303030-1234-4567-8901-abcdef012348',
|
||||
};
|
||||
|
||||
type WorkspaceSeedIds = {
|
||||
userWorkspaceIds: {
|
||||
TIM: string;
|
||||
JONY: string;
|
||||
PHIL: string;
|
||||
JANE: string;
|
||||
};
|
||||
connectedAccountIds: typeof CONNECTED_ACCOUNT_DATA_SEED_IDS;
|
||||
messageChannelIds: typeof MESSAGE_CHANNEL_DATA_SEED_IDS;
|
||||
calendarChannelIds: typeof CALENDAR_CHANNEL_DATA_SEED_IDS;
|
||||
messageFolderIds: typeof MESSAGE_FOLDER_DATA_SEED_IDS;
|
||||
};
|
||||
|
||||
const getSeedIds = (workspaceId: string): WorkspaceSeedIds => {
|
||||
if (workspaceId === SEED_YCOMBINATOR_WORKSPACE_ID) {
|
||||
return {
|
||||
userWorkspaceIds: {
|
||||
TIM: USER_WORKSPACE_DATA_SEED_IDS.TIM_ACME,
|
||||
JONY: USER_WORKSPACE_DATA_SEED_IDS.JONY_ACME,
|
||||
PHIL: USER_WORKSPACE_DATA_SEED_IDS.PHIL_ACME,
|
||||
JANE: USER_WORKSPACE_DATA_SEED_IDS.JANE_ACME,
|
||||
},
|
||||
connectedAccountIds: YC_CONNECTED_ACCOUNT_IDS,
|
||||
messageChannelIds: YC_MESSAGE_CHANNEL_IDS,
|
||||
calendarChannelIds: YC_CALENDAR_CHANNEL_IDS,
|
||||
messageFolderIds: YC_MESSAGE_FOLDER_IDS,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
userWorkspaceIds: {
|
||||
TIM: USER_WORKSPACE_DATA_SEED_IDS.TIM,
|
||||
JONY: USER_WORKSPACE_DATA_SEED_IDS.JONY,
|
||||
PHIL: USER_WORKSPACE_DATA_SEED_IDS.PHIL,
|
||||
JANE: USER_WORKSPACE_DATA_SEED_IDS.JANE,
|
||||
},
|
||||
connectedAccountIds: CONNECTED_ACCOUNT_DATA_SEED_IDS,
|
||||
messageChannelIds: MESSAGE_CHANNEL_DATA_SEED_IDS,
|
||||
calendarChannelIds: CALENDAR_CHANNEL_DATA_SEED_IDS,
|
||||
messageFolderIds: MESSAGE_FOLDER_DATA_SEED_IDS,
|
||||
};
|
||||
};
|
||||
|
||||
export const seedMetadataEntities = async ({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
@@ -36,10 +114,6 @@ export const seedMetadataEntities = async ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (workspaceId === SEED_YCOMBINATOR_WORKSPACE_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
await seedConnectedAccounts({ queryRunner, schemaName, workspaceId });
|
||||
await seedMessageChannels({ queryRunner, schemaName, workspaceId });
|
||||
await seedCalendarChannels({ queryRunner, schemaName, workspaceId });
|
||||
@@ -51,40 +125,42 @@ const seedConnectedAccounts = async ({
|
||||
schemaName,
|
||||
workspaceId,
|
||||
}: SeedMetadataEntitiesArgs) => {
|
||||
const ids = getSeedIds(workspaceId);
|
||||
|
||||
const connectedAccounts = [
|
||||
{
|
||||
id: CONNECTED_ACCOUNT_DATA_SEED_IDS.TIM,
|
||||
id: ids.connectedAccountIds.TIM,
|
||||
handle: 'tim@apple.dev',
|
||||
provider: 'google',
|
||||
userWorkspaceId: USER_WORKSPACE_DATA_SEED_IDS.TIM,
|
||||
userWorkspaceId: ids.userWorkspaceIds.TIM,
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: CONNECTED_ACCOUNT_DATA_SEED_IDS.JONY,
|
||||
id: ids.connectedAccountIds.JONY,
|
||||
handle: 'jony.ive@apple.dev',
|
||||
provider: 'google',
|
||||
userWorkspaceId: USER_WORKSPACE_DATA_SEED_IDS.JONY,
|
||||
userWorkspaceId: ids.userWorkspaceIds.JONY,
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: CONNECTED_ACCOUNT_DATA_SEED_IDS.PHIL,
|
||||
id: ids.connectedAccountIds.PHIL,
|
||||
handle: 'phil.schiler@apple.dev',
|
||||
provider: 'google',
|
||||
userWorkspaceId: USER_WORKSPACE_DATA_SEED_IDS.PHIL,
|
||||
userWorkspaceId: ids.userWorkspaceIds.PHIL,
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: CONNECTED_ACCOUNT_DATA_SEED_IDS.JANE,
|
||||
id: ids.connectedAccountIds.JANE,
|
||||
handle: 'jane.austen@apple.dev',
|
||||
provider: 'google',
|
||||
userWorkspaceId: USER_WORKSPACE_DATA_SEED_IDS.JANE,
|
||||
userWorkspaceId: ids.userWorkspaceIds.JANE,
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: CONNECTED_ACCOUNT_DATA_SEED_IDS.JANE_DELETABLE,
|
||||
id: ids.connectedAccountIds.JANE_DELETABLE,
|
||||
handle: 'jane-deletable@apple.dev',
|
||||
provider: 'google',
|
||||
userWorkspaceId: USER_WORKSPACE_DATA_SEED_IDS.JANE,
|
||||
userWorkspaceId: ids.userWorkspaceIds.JANE,
|
||||
workspaceId,
|
||||
},
|
||||
];
|
||||
@@ -109,9 +185,11 @@ const seedMessageChannels = async ({
|
||||
schemaName,
|
||||
workspaceId,
|
||||
}: SeedMetadataEntitiesArgs) => {
|
||||
const ids = getSeedIds(workspaceId);
|
||||
|
||||
const messageChannels = [
|
||||
{
|
||||
id: MESSAGE_CHANNEL_DATA_SEED_IDS.TIM,
|
||||
id: ids.messageChannelIds.TIM,
|
||||
handle: 'tim@apple.dev',
|
||||
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
|
||||
type: MessageChannelType.EMAIL,
|
||||
@@ -123,11 +201,11 @@ const seedMessageChannels = async ({
|
||||
excludeGroupEmails: false,
|
||||
pendingGroupEmailsAction: 'NONE',
|
||||
isSyncEnabled: true,
|
||||
connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.TIM,
|
||||
connectedAccountId: ids.connectedAccountIds.TIM,
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: MESSAGE_CHANNEL_DATA_SEED_IDS.JONY,
|
||||
id: ids.messageChannelIds.JONY,
|
||||
handle: 'jony.ive@apple.dev',
|
||||
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
|
||||
type: MessageChannelType.EMAIL,
|
||||
@@ -139,11 +217,27 @@ const seedMessageChannels = async ({
|
||||
excludeGroupEmails: false,
|
||||
pendingGroupEmailsAction: 'NONE',
|
||||
isSyncEnabled: true,
|
||||
connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.JONY,
|
||||
connectedAccountId: ids.connectedAccountIds.JONY,
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: MESSAGE_CHANNEL_DATA_SEED_IDS.JANE,
|
||||
id: ids.messageChannelIds.PHIL,
|
||||
handle: 'phil.schiler@apple.dev',
|
||||
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
|
||||
type: MessageChannelType.EMAIL,
|
||||
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
|
||||
isContactAutoCreationEnabled: true,
|
||||
contactAutoCreationPolicy: 'SENT_AND_RECEIVED',
|
||||
messageFolderImportPolicy: 'ALL_FOLDERS',
|
||||
excludeNonProfessionalEmails: false,
|
||||
excludeGroupEmails: false,
|
||||
pendingGroupEmailsAction: 'NONE',
|
||||
isSyncEnabled: true,
|
||||
connectedAccountId: ids.connectedAccountIds.PHIL,
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: ids.messageChannelIds.JANE,
|
||||
handle: 'jane.austen@apple.dev',
|
||||
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
|
||||
type: MessageChannelType.EMAIL,
|
||||
@@ -155,7 +249,39 @@ const seedMessageChannels = async ({
|
||||
excludeGroupEmails: false,
|
||||
pendingGroupEmailsAction: 'NONE',
|
||||
isSyncEnabled: true,
|
||||
connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.JANE,
|
||||
connectedAccountId: ids.connectedAccountIds.JANE,
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: ids.messageChannelIds.SUPPORT,
|
||||
handle: 'support@apple.dev',
|
||||
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
|
||||
type: MessageChannelType.EMAIL,
|
||||
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
|
||||
isContactAutoCreationEnabled: true,
|
||||
contactAutoCreationPolicy: 'SENT_AND_RECEIVED',
|
||||
messageFolderImportPolicy: 'ALL_FOLDERS',
|
||||
excludeNonProfessionalEmails: false,
|
||||
excludeGroupEmails: false,
|
||||
pendingGroupEmailsAction: 'NONE',
|
||||
isSyncEnabled: true,
|
||||
connectedAccountId: ids.connectedAccountIds.TIM,
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: ids.messageChannelIds.SALES,
|
||||
handle: 'sales@apple.dev',
|
||||
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
|
||||
type: MessageChannelType.EMAIL,
|
||||
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
|
||||
isContactAutoCreationEnabled: true,
|
||||
contactAutoCreationPolicy: 'SENT_AND_RECEIVED',
|
||||
messageFolderImportPolicy: 'ALL_FOLDERS',
|
||||
excludeNonProfessionalEmails: false,
|
||||
excludeGroupEmails: false,
|
||||
pendingGroupEmailsAction: 'NONE',
|
||||
isSyncEnabled: true,
|
||||
connectedAccountId: ids.connectedAccountIds.TIM,
|
||||
workspaceId,
|
||||
},
|
||||
];
|
||||
@@ -189,38 +315,73 @@ const seedCalendarChannels = async ({
|
||||
schemaName,
|
||||
workspaceId,
|
||||
}: SeedMetadataEntitiesArgs) => {
|
||||
const ids = getSeedIds(workspaceId);
|
||||
|
||||
const calendarChannels = [
|
||||
{
|
||||
id: CALENDAR_CHANNEL_DATA_SEED_IDS.TIM,
|
||||
id: ids.calendarChannelIds.TIM,
|
||||
handle: 'tim@apple.dev',
|
||||
visibility: CalendarChannelVisibility.METADATA,
|
||||
syncStage: 'CALENDAR_EVENT_LIST_FETCH_PENDING',
|
||||
isContactAutoCreationEnabled: true,
|
||||
contactAutoCreationPolicy: 'NONE',
|
||||
isSyncEnabled: true,
|
||||
connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.TIM,
|
||||
connectedAccountId: ids.connectedAccountIds.TIM,
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: CALENDAR_CHANNEL_DATA_SEED_IDS.JONY,
|
||||
id: ids.calendarChannelIds.JONY,
|
||||
handle: 'jony@apple.dev',
|
||||
visibility: CalendarChannelVisibility.SHARE_EVERYTHING,
|
||||
syncStage: 'CALENDAR_EVENT_LIST_FETCH_PENDING',
|
||||
isContactAutoCreationEnabled: true,
|
||||
contactAutoCreationPolicy: 'NONE',
|
||||
isSyncEnabled: true,
|
||||
connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.JONY,
|
||||
connectedAccountId: ids.connectedAccountIds.JONY,
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: CALENDAR_CHANNEL_DATA_SEED_IDS.JANE,
|
||||
id: ids.calendarChannelIds.PHIL,
|
||||
handle: 'phil@apple.dev',
|
||||
visibility: CalendarChannelVisibility.METADATA,
|
||||
syncStage: 'CALENDAR_EVENT_LIST_FETCH_PENDING',
|
||||
isContactAutoCreationEnabled: true,
|
||||
contactAutoCreationPolicy: 'NONE',
|
||||
isSyncEnabled: true,
|
||||
connectedAccountId: ids.connectedAccountIds.PHIL,
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: ids.calendarChannelIds.JANE,
|
||||
handle: 'jane.austen@apple.dev',
|
||||
visibility: CalendarChannelVisibility.SHARE_EVERYTHING,
|
||||
syncStage: 'CALENDAR_EVENT_LIST_FETCH_PENDING',
|
||||
isContactAutoCreationEnabled: true,
|
||||
contactAutoCreationPolicy: 'NONE',
|
||||
isSyncEnabled: true,
|
||||
connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.JANE,
|
||||
connectedAccountId: ids.connectedAccountIds.JANE,
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: ids.calendarChannelIds.COMPANY_MAIN,
|
||||
handle: 'company-main@apple.dev',
|
||||
visibility: CalendarChannelVisibility.SHARE_EVERYTHING,
|
||||
syncStage: 'CALENDAR_EVENT_LIST_FETCH_PENDING',
|
||||
isContactAutoCreationEnabled: true,
|
||||
contactAutoCreationPolicy: 'NONE',
|
||||
isSyncEnabled: true,
|
||||
connectedAccountId: ids.connectedAccountIds.TIM,
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: ids.calendarChannelIds.TEAM_CALENDAR,
|
||||
handle: 'team-calendar@apple.dev',
|
||||
visibility: CalendarChannelVisibility.SHARE_EVERYTHING,
|
||||
syncStage: 'CALENDAR_EVENT_LIST_FETCH_PENDING',
|
||||
isContactAutoCreationEnabled: true,
|
||||
contactAutoCreationPolicy: 'NONE',
|
||||
isSyncEnabled: true,
|
||||
connectedAccountId: ids.connectedAccountIds.TIM,
|
||||
workspaceId,
|
||||
},
|
||||
];
|
||||
@@ -249,40 +410,42 @@ const seedMessageFolders = async ({
|
||||
schemaName,
|
||||
workspaceId,
|
||||
}: SeedMetadataEntitiesArgs) => {
|
||||
const ids = getSeedIds(workspaceId);
|
||||
|
||||
const messageFolders = [
|
||||
{
|
||||
id: MESSAGE_FOLDER_DATA_SEED_IDS.TIM_INBOX,
|
||||
id: ids.messageFolderIds.TIM_INBOX,
|
||||
name: 'INBOX',
|
||||
isSynced: true,
|
||||
isSentFolder: false,
|
||||
messageChannelId: MESSAGE_CHANNEL_DATA_SEED_IDS.TIM,
|
||||
messageChannelId: ids.messageChannelIds.TIM,
|
||||
workspaceId,
|
||||
pendingSyncAction: MessageFolderPendingSyncAction.NONE,
|
||||
},
|
||||
{
|
||||
id: MESSAGE_FOLDER_DATA_SEED_IDS.JONY_INBOX,
|
||||
id: ids.messageFolderIds.JONY_INBOX,
|
||||
name: 'INBOX',
|
||||
isSynced: true,
|
||||
isSentFolder: false,
|
||||
messageChannelId: MESSAGE_CHANNEL_DATA_SEED_IDS.JONY,
|
||||
messageChannelId: ids.messageChannelIds.JONY,
|
||||
workspaceId,
|
||||
pendingSyncAction: MessageFolderPendingSyncAction.NONE,
|
||||
},
|
||||
{
|
||||
id: MESSAGE_FOLDER_DATA_SEED_IDS.JANE_INBOX,
|
||||
id: ids.messageFolderIds.JANE_INBOX,
|
||||
name: 'INBOX',
|
||||
isSynced: true,
|
||||
isSentFolder: false,
|
||||
messageChannelId: MESSAGE_CHANNEL_DATA_SEED_IDS.JANE,
|
||||
messageChannelId: ids.messageChannelIds.JANE,
|
||||
workspaceId,
|
||||
pendingSyncAction: MessageFolderPendingSyncAction.NONE,
|
||||
},
|
||||
{
|
||||
id: MESSAGE_FOLDER_DATA_SEED_IDS.JANE_SENT,
|
||||
id: ids.messageFolderIds.JANE_SENT,
|
||||
name: 'Sent',
|
||||
isSynced: true,
|
||||
isSentFolder: true,
|
||||
messageChannelId: MESSAGE_CHANNEL_DATA_SEED_IDS.JANE,
|
||||
messageChannelId: ids.messageChannelIds.JANE,
|
||||
workspaceId,
|
||||
pendingSyncAction: MessageFolderPendingSyncAction.NONE,
|
||||
},
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { CalendarChannelVisibility } from 'twenty-shared/types';
|
||||
import { CONNECTED_ACCOUNT_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/connected-account-data-seeds.constant';
|
||||
import { CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
|
||||
type CalendarChannelDataSeed = {
|
||||
id: string;
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
import { CONNECTED_ACCOUNT_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/connected-account-data-seeds.constant';
|
||||
import {
|
||||
MessageChannelSyncStage,
|
||||
MessageChannelType,
|
||||
MessageChannelVisibility,
|
||||
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
} from 'twenty-shared/types';
|
||||
import { CONNECTED_ACCOUNT_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/connected-account-data-seeds.constant';
|
||||
|
||||
type MessageChannelDataSeed = {
|
||||
id: string;
|
||||
|
||||
-36
@@ -20,10 +20,6 @@ import {
|
||||
type AttachmentFileSeedMetadata,
|
||||
generateAttachmentSeedsForWorkspace,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/data/constants/attachment-data-seeds.constant';
|
||||
import {
|
||||
CALENDAR_CHANNEL_DATA_SEED_COLUMNS,
|
||||
CALENDAR_CHANNEL_DATA_SEEDS,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/data/constants/calendar-channel-data-seeds.constant';
|
||||
import {
|
||||
CALENDAR_CHANNEL_EVENT_ASSOCIATION_DATA_SEED_COLUMNS,
|
||||
CALENDAR_CHANNEL_EVENT_ASSOCIATION_DATA_SEEDS,
|
||||
@@ -40,10 +36,6 @@ import {
|
||||
COMPANY_DATA_SEED_COLUMNS,
|
||||
COMPANY_DATA_SEEDS,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/data/constants/company-data-seeds.constant';
|
||||
import {
|
||||
CONNECTED_ACCOUNT_DATA_SEED_COLUMNS,
|
||||
CONNECTED_ACCOUNT_DATA_SEEDS,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/data/constants/connected-account-data-seeds.constant';
|
||||
import {
|
||||
DASHBOARD_DATA_SEED_COLUMNS,
|
||||
getDashboardDataSeeds,
|
||||
@@ -52,10 +44,6 @@ import {
|
||||
EMPLOYMENT_HISTORY_DATA_SEED_COLUMNS,
|
||||
EMPLOYMENT_HISTORY_DATA_SEEDS,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/data/constants/employment-history-data-seeds.constant';
|
||||
import {
|
||||
MESSAGE_CHANNEL_DATA_SEED_COLUMNS,
|
||||
MESSAGE_CHANNEL_DATA_SEEDS,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/data/constants/message-channel-data-seeds.constant';
|
||||
import {
|
||||
MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_DATA_SEED_COLUMNS,
|
||||
MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_DATA_SEEDS,
|
||||
@@ -64,10 +52,6 @@ import {
|
||||
MESSAGE_DATA_SEED_COLUMNS,
|
||||
MESSAGE_DATA_SEEDS,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/data/constants/message-data-seeds.constant';
|
||||
import {
|
||||
MESSAGE_FOLDER_DATA_SEED_COLUMNS,
|
||||
MESSAGE_FOLDER_DATA_SEEDS,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/data/constants/message-folder-data-seeds.constant';
|
||||
import {
|
||||
getMessageParticipantDataSeeds,
|
||||
MESSAGE_PARTICIPANT_DATA_SEED_COLUMNS,
|
||||
@@ -168,11 +152,6 @@ const getRecordSeedsBatches = (
|
||||
pgColumns: COMPANY_DATA_SEED_COLUMNS,
|
||||
recordSeeds: COMPANY_DATA_SEEDS,
|
||||
},
|
||||
{
|
||||
tableName: 'connectedAccount',
|
||||
pgColumns: CONNECTED_ACCOUNT_DATA_SEED_COLUMNS,
|
||||
recordSeeds: CONNECTED_ACCOUNT_DATA_SEEDS,
|
||||
},
|
||||
{
|
||||
tableName: 'dashboard',
|
||||
pgColumns: DASHBOARD_DATA_SEED_COLUMNS,
|
||||
@@ -192,25 +171,10 @@ const getRecordSeedsBatches = (
|
||||
pgColumns: PET_DATA_SEED_COLUMNS,
|
||||
recordSeeds: PET_DATA_SEEDS,
|
||||
},
|
||||
{
|
||||
tableName: 'calendarChannel',
|
||||
pgColumns: CALENDAR_CHANNEL_DATA_SEED_COLUMNS,
|
||||
recordSeeds: CALENDAR_CHANNEL_DATA_SEEDS,
|
||||
},
|
||||
{
|
||||
tableName: 'messageChannel',
|
||||
pgColumns: MESSAGE_CHANNEL_DATA_SEED_COLUMNS,
|
||||
recordSeeds: MESSAGE_CHANNEL_DATA_SEEDS,
|
||||
},
|
||||
];
|
||||
|
||||
// Batch 4: Depends on person/company/messageChannel or independent
|
||||
const batch4: RecordSeedConfig[] = [
|
||||
{
|
||||
tableName: 'messageFolder',
|
||||
pgColumns: MESSAGE_FOLDER_DATA_SEED_COLUMNS,
|
||||
recordSeeds: MESSAGE_FOLDER_DATA_SEEDS,
|
||||
},
|
||||
{
|
||||
tableName: 'opportunity',
|
||||
pgColumns: OPPORTUNITY_DATA_SEED_COLUMNS,
|
||||
|
||||
+1858
-1870
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -17,8 +17,8 @@ import { buildDashboardStandardFlatFieldMetadatas } from 'src/engine/workspace-m
|
||||
import { buildFavoriteFolderStandardFlatFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-favorite-folder-standard-flat-field-metadata.util';
|
||||
import { buildFavoriteStandardFlatFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-favorite-standard-flat-field-metadata.util';
|
||||
import { buildMessageChannelMessageAssociationMessageFolderStandardFlatFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-message-channel-message-association-message-folder-standard-flat-field-metadata.util';
|
||||
import { buildMessageChannelMessageAssociationStandardFlatFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-message-channel-message-association-standard-flat-field-metadata.util';
|
||||
import { buildMessageChannelStandardFlatFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-message-channel-standard-flat-field-metadata.util';
|
||||
import { buildMessageChannelMessageAssociationStandardFlatFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-message-channel-message-association-standard-flat-field-metadata.util';
|
||||
import { buildMessageFolderStandardFlatFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-message-folder-standard-flat-field-metadata.util';
|
||||
import { buildMessageParticipantStandardFlatFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-message-participant-standard-flat-field-metadata.util';
|
||||
import { buildMessageStandardFlatFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-message-standard-flat-field-metadata.util';
|
||||
@@ -44,9 +44,9 @@ type StandardFieldBuilder<P extends AllStandardObjectName> = (
|
||||
const STANDARD_FLAT_FIELD_METADATA_BUILDERS_BY_OBJECT_NAME = {
|
||||
attachment: buildAttachmentStandardFlatFieldMetadatas,
|
||||
blocklist: buildBlocklistStandardFlatFieldMetadatas,
|
||||
calendarChannel: buildCalendarChannelStandardFlatFieldMetadatas,
|
||||
calendarChannelEventAssociation:
|
||||
buildCalendarChannelEventAssociationStandardFlatFieldMetadatas,
|
||||
calendarChannel: buildCalendarChannelStandardFlatFieldMetadatas,
|
||||
calendarEventParticipant:
|
||||
buildCalendarEventParticipantStandardFlatFieldMetadatas,
|
||||
calendarEvent: buildCalendarEventStandardFlatFieldMetadatas,
|
||||
|
||||
+3
-11
@@ -234,25 +234,17 @@ export const buildCalendarChannelEventAssociationStandardFlatFieldMetadatas = ({
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
calendarChannel: createStandardRelationFieldFlatMetadata({
|
||||
calendarChannelId: createStandardFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
type: FieldMetadataType.RELATION,
|
||||
morphId: null,
|
||||
fieldName: 'calendarChannel',
|
||||
fieldName: 'calendarChannelId',
|
||||
type: FieldMetadataType.UUID,
|
||||
label: i18nLabel(msg`Channel ID`),
|
||||
description: i18nLabel(msg`Channel ID`),
|
||||
icon: 'IconCalendar',
|
||||
isNullable: false,
|
||||
isUIReadOnly: true,
|
||||
targetObjectName: 'calendarChannel',
|
||||
targetFieldName: 'calendarChannelEventAssociations',
|
||||
settings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
joinColumnName: 'calendarChannelId',
|
||||
},
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
|
||||
-23
@@ -561,27 +561,4 @@ export const buildCalendarChannelStandardFlatFieldMetadatas = ({
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
calendarChannelEventAssociations: createStandardRelationFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
type: FieldMetadataType.RELATION,
|
||||
morphId: null,
|
||||
fieldName: 'calendarChannelEventAssociations',
|
||||
label: i18nLabel(msg`Calendar Channel Event Associations`),
|
||||
description: i18nLabel(msg`Calendar Channel Event Associations`),
|
||||
icon: 'IconCalendar',
|
||||
isNullable: false,
|
||||
isUIReadOnly: true,
|
||||
targetObjectName: 'calendarChannelEventAssociation',
|
||||
targetFieldName: 'calendarChannel',
|
||||
settings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
});
|
||||
|
||||
+3
-11
@@ -229,25 +229,17 @@ export const buildMessageChannelMessageAssociationMessageFolderStandardFlatField
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
messageFolder: createStandardRelationFieldFlatMetadata({
|
||||
messageFolderId: createStandardFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
type: FieldMetadataType.RELATION,
|
||||
morphId: null,
|
||||
fieldName: 'messageFolder',
|
||||
fieldName: 'messageFolderId',
|
||||
type: FieldMetadataType.UUID,
|
||||
label: i18nLabel(msg`Message Folder`),
|
||||
description: i18nLabel(msg`Message Folder`),
|
||||
icon: 'IconFolder',
|
||||
isNullable: false,
|
||||
isUIReadOnly: true,
|
||||
targetObjectName: 'messageFolder',
|
||||
targetFieldName: 'messageChannelMessageAssociationMessageFolders',
|
||||
settings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
joinColumnName: 'messageFolderId',
|
||||
},
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
|
||||
+3
-11
@@ -273,25 +273,17 @@ export const buildMessageChannelMessageAssociationStandardFlatFieldMetadatas =
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
messageChannel: createStandardRelationFieldFlatMetadata({
|
||||
messageChannelId: createStandardFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
type: FieldMetadataType.RELATION,
|
||||
morphId: null,
|
||||
fieldName: 'messageChannel',
|
||||
fieldName: 'messageChannelId',
|
||||
type: FieldMetadataType.UUID,
|
||||
label: i18nLabel(msg`Message Channel Id`),
|
||||
description: i18nLabel(msg`Message Channel Id`),
|
||||
icon: 'IconHash',
|
||||
isNullable: true,
|
||||
isUIReadOnly: true,
|
||||
targetObjectName: 'messageChannel',
|
||||
targetFieldName: 'messageChannelMessageAssociations',
|
||||
settings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
joinColumnName: 'messageChannelId',
|
||||
},
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
|
||||
-46
@@ -724,50 +724,4 @@ export const buildMessageChannelStandardFlatFieldMetadatas = ({
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
messageChannelMessageAssociations: createStandardRelationFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
type: FieldMetadataType.RELATION,
|
||||
morphId: null,
|
||||
fieldName: 'messageChannelMessageAssociations',
|
||||
label: i18nLabel(msg`Message Channel Association`),
|
||||
description: i18nLabel(msg`Messages from the channel.`),
|
||||
icon: 'IconMessage',
|
||||
isNullable: true,
|
||||
isUIReadOnly: true,
|
||||
targetObjectName: 'messageChannelMessageAssociation',
|
||||
targetFieldName: 'messageChannel',
|
||||
settings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
messageFolders: createStandardRelationFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
type: FieldMetadataType.RELATION,
|
||||
morphId: null,
|
||||
fieldName: 'messageFolders',
|
||||
label: i18nLabel(msg`Message Folders`),
|
||||
description: i18nLabel(msg`Message Folders`),
|
||||
icon: 'IconFolder',
|
||||
isNullable: true,
|
||||
isUIReadOnly: true,
|
||||
targetObjectName: 'messageFolder',
|
||||
targetFieldName: 'messageChannel',
|
||||
settings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
});
|
||||
|
||||
+4
-44
@@ -1,11 +1,6 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { i18nLabel } from 'src/engine/workspace-manager/twenty-standard-application/utils/i18n-label.util';
|
||||
import {
|
||||
DateDisplayFormat,
|
||||
FieldMetadataType,
|
||||
RelationOnDeleteAction,
|
||||
RelationType,
|
||||
} from 'twenty-shared/types';
|
||||
import { DateDisplayFormat, FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type AllStandardObjectFieldName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-field-name.type';
|
||||
@@ -13,7 +8,6 @@ import {
|
||||
type CreateStandardFieldArgs,
|
||||
createStandardFieldFlatMetadata,
|
||||
} from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-field-flat-metadata.util';
|
||||
import { createStandardRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util';
|
||||
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
|
||||
import { SEARCH_FIELDS_FOR_MESSAGE_FOLDER } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
|
||||
|
||||
@@ -333,55 +327,21 @@ export const buildMessageFolderStandardFlatFieldMetadatas = ({
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
messageChannel: createStandardRelationFieldFlatMetadata({
|
||||
messageChannelId: createStandardFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
type: FieldMetadataType.RELATION,
|
||||
morphId: null,
|
||||
fieldName: 'messageChannel',
|
||||
fieldName: 'messageChannelId',
|
||||
type: FieldMetadataType.UUID,
|
||||
label: i18nLabel(msg`Message Channel`),
|
||||
description: i18nLabel(msg`Message Channel`),
|
||||
icon: 'IconMessage',
|
||||
isNullable: false,
|
||||
isUIReadOnly: true,
|
||||
targetObjectName: 'messageChannel',
|
||||
targetFieldName: 'messageFolders',
|
||||
settings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
joinColumnName: 'messageChannelId',
|
||||
},
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
messageChannelMessageAssociationMessageFolders:
|
||||
createStandardRelationFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
type: FieldMetadataType.RELATION,
|
||||
morphId: null,
|
||||
fieldName: 'messageChannelMessageAssociationMessageFolders',
|
||||
label: i18nLabel(msg`Message Association Folders`),
|
||||
description: i18nLabel(
|
||||
msg`Message Association Folders (supports multiple folders/labels)`,
|
||||
),
|
||||
icon: 'IconFolders',
|
||||
isNullable: true,
|
||||
isUIReadOnly: true,
|
||||
targetObjectName: 'messageChannelMessageAssociationMessageFolder',
|
||||
targetFieldName: 'messageFolder',
|
||||
settings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
});
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ export const buildCalendarChannelEventAssociationStandardFlatIndexMetadatas = ({
|
||||
workspaceId,
|
||||
context: {
|
||||
indexName: 'calendarChannelIdIndex',
|
||||
relatedFieldNames: ['calendarChannel'],
|
||||
relatedFieldNames: ['calendarChannelId'],
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
|
||||
+2
-2
@@ -37,7 +37,7 @@ export const buildMessageChannelMessageAssociationMessageFolderStandardFlatIndex
|
||||
workspaceId,
|
||||
context: {
|
||||
indexName: 'messageFolderIdIndex',
|
||||
relatedFieldNames: ['messageFolder'],
|
||||
relatedFieldNames: ['messageFolderId'],
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
@@ -53,7 +53,7 @@ export const buildMessageChannelMessageAssociationMessageFolderStandardFlatIndex
|
||||
'messageChannelMessageAssociationIdMessageFolderIdUniqueIndex',
|
||||
relatedFieldNames: [
|
||||
'messageChannelMessageAssociation',
|
||||
'messageFolder',
|
||||
'messageFolderId',
|
||||
],
|
||||
isUnique: true,
|
||||
indexWhereClause: '"deletedAt" IS NULL',
|
||||
|
||||
+2
-2
@@ -25,7 +25,7 @@ export const buildMessageChannelMessageAssociationStandardFlatIndexMetadatas =
|
||||
workspaceId,
|
||||
context: {
|
||||
indexName: 'messageChannelIdIndex',
|
||||
relatedFieldNames: ['messageChannel'],
|
||||
relatedFieldNames: ['messageChannelId'],
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
@@ -49,7 +49,7 @@ export const buildMessageChannelMessageAssociationStandardFlatIndexMetadatas =
|
||||
workspaceId,
|
||||
context: {
|
||||
indexName: 'messageChannelIdMessageIdUniqueIndex',
|
||||
relatedFieldNames: ['messageChannel', 'message'],
|
||||
relatedFieldNames: ['messageChannelId', 'message'],
|
||||
isUnique: true,
|
||||
indexWhereClause: '"deletedAt" IS NULL',
|
||||
},
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ export const buildMessageFolderStandardFlatIndexMetadatas = ({
|
||||
workspaceId,
|
||||
context: {
|
||||
indexName: 'messageChannelIdIndex',
|
||||
relatedFieldNames: ['messageChannel'],
|
||||
relatedFieldNames: ['messageChannelId'],
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
|
||||
+4
-4
@@ -17,8 +17,8 @@ export const computeStandardCalendarChannelEventAssociationViewFields = (
|
||||
objectName: 'calendarChannelEventAssociation',
|
||||
context: {
|
||||
viewName: 'allCalendarChannelEventAssociations',
|
||||
viewFieldName: 'calendarChannel',
|
||||
fieldName: 'calendarChannel',
|
||||
viewFieldName: 'calendarChannelId',
|
||||
fieldName: 'calendarChannelId',
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
@@ -70,8 +70,8 @@ export const computeStandardCalendarChannelEventAssociationViewFields = (
|
||||
objectName: 'calendarChannelEventAssociation',
|
||||
context: {
|
||||
viewName: 'calendarChannelEventAssociationRecordPageFields',
|
||||
viewFieldName: 'calendarChannel',
|
||||
fieldName: 'calendarChannel',
|
||||
viewFieldName: 'calendarChannelId',
|
||||
fieldName: 'calendarChannelId',
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
|
||||
+4
-4
@@ -31,8 +31,8 @@ export const computeStandardMessageChannelMessageAssociationMessageFolderViewFie
|
||||
objectName: 'messageChannelMessageAssociationMessageFolder',
|
||||
context: {
|
||||
viewName: 'allMessageChannelMessageAssociationMessageFolders',
|
||||
viewFieldName: 'messageFolder',
|
||||
fieldName: 'messageFolder',
|
||||
viewFieldName: 'messageFolderId',
|
||||
fieldName: 'messageFolderId',
|
||||
position: 1,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
@@ -74,8 +74,8 @@ export const computeStandardMessageChannelMessageAssociationMessageFolderViewFie
|
||||
context: {
|
||||
viewName:
|
||||
'messageChannelMessageAssociationMessageFolderRecordPageFields',
|
||||
viewFieldName: 'messageFolder',
|
||||
fieldName: 'messageFolder',
|
||||
viewFieldName: 'messageFolderId',
|
||||
fieldName: 'messageFolderId',
|
||||
position: 1,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
|
||||
+4
-4
@@ -17,8 +17,8 @@ export const computeStandardMessageChannelMessageAssociationViewFields = (
|
||||
objectName: 'messageChannelMessageAssociation',
|
||||
context: {
|
||||
viewName: 'allMessageChannelMessageAssociations',
|
||||
viewFieldName: 'messageChannel',
|
||||
fieldName: 'messageChannel',
|
||||
viewFieldName: 'messageChannelId',
|
||||
fieldName: 'messageChannelId',
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
@@ -83,8 +83,8 @@ export const computeStandardMessageChannelMessageAssociationViewFields = (
|
||||
objectName: 'messageChannelMessageAssociation',
|
||||
context: {
|
||||
viewName: 'messageChannelMessageAssociationRecordPageFields',
|
||||
viewFieldName: 'messageChannel',
|
||||
fieldName: 'messageChannel',
|
||||
viewFieldName: 'messageChannelId',
|
||||
fieldName: 'messageChannelId',
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
|
||||
+4
-4
@@ -25,8 +25,8 @@ export const computeStandardMessageFolderViewFields = (
|
||||
objectName: 'messageFolder',
|
||||
context: {
|
||||
viewName: 'allMessageFolders',
|
||||
viewFieldName: 'messageChannel',
|
||||
fieldName: 'messageChannel',
|
||||
viewFieldName: 'messageChannelId',
|
||||
fieldName: 'messageChannelId',
|
||||
position: 1,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
@@ -75,8 +75,8 @@ export const computeStandardMessageFolderViewFields = (
|
||||
objectName: 'messageFolder',
|
||||
context: {
|
||||
viewName: 'messageFolderRecordPageFields',
|
||||
viewFieldName: 'messageChannel',
|
||||
fieldName: 'messageChannel',
|
||||
viewFieldName: 'messageChannelId',
|
||||
fieldName: 'messageChannelId',
|
||||
position: 1,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
|
||||
-1
@@ -1,6 +1,5 @@
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
|
||||
export const DEFAULT_FEATURE_FLAGS = [
|
||||
FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED,
|
||||
FeatureFlagKey.IS_DATASOURCE_MIGRATED,
|
||||
] as const satisfies FeatureFlagKey[];
|
||||
|
||||
Reference in New Issue
Block a user