feat: migrate ConnectedAccount infrastructure entities to metadata schema (#18784)
## Summary - Migrates 4 entities (`connectedAccount`, `messageChannel`, `calendarChannel`, `messageFolder`) from per-workspace schemas to the shared `core` metadata schema - Introduces a `IS_CONNECTED_ACCOUNT_MIGRATED` feature flag to control the migration: when enabled, reads come from core metadata and all writes are dual-written to both workspace and core - Extracts 12 enums from workspace entity files to `twenty-shared` for reuse across frontend and backend - Creates new TypeORM entities, metadata services, GraphQL resolvers/DTOs, and exception interceptors per entity - Each entity owns its own data access module (`ConnectedAccountDataAccessModule`, `MessageChannelDataAccessModule`, `CalendarChannelDataAccessModule`, `MessageFolderDataAccessModule`) — no umbrella infrastructure module - Adds a 1.20 upgrade command that backfills data from workspace schemas to core (preserving UUIDs) and enables the feature flag - Replaces direct repository access with data access service calls across ~50 files in messaging, calendar, and connected-account modules - Adds `lastSignedInAt` and `oidcTokenClaims` fields to the new `ConnectedAccountEntity` - Drops unused `lastSyncHistoryId` field from the migrated connected account entity ## Test plan - [x] Lint passes (`npx nx lint:diff-with-main twenty-server`) - [x] Typecheck passes (`npx nx typecheck twenty-server`) - [x] All unit tests pass (477 suites, 4267 tests, 0 failures) - [ ] Manual test: verify messaging sync works with feature flag disabled (existing behavior) - [ ] Manual test: run upgrade command on a workspace, verify data backfilled to core tables - [ ] Manual test: verify messaging/calendar sync works with feature flag enabled (dual-write path) - [ ] Manual test: verify GraphQL metadata resolvers return correct data when flag enabled
This commit is contained in:
@@ -761,6 +761,56 @@ export type BooleanFieldComparison = {
|
||||
isNot?: InputMaybe<Scalars['Boolean']>;
|
||||
};
|
||||
|
||||
export type CalendarChannel = {
|
||||
__typename?: 'CalendarChannel';
|
||||
connectedAccountId: Scalars['UUID'];
|
||||
contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy;
|
||||
createdAt: Scalars['DateTime'];
|
||||
handle: Scalars['String'];
|
||||
id: Scalars['UUID'];
|
||||
isContactAutoCreationEnabled: Scalars['Boolean'];
|
||||
isSyncEnabled: Scalars['Boolean'];
|
||||
syncCursor?: Maybe<Scalars['String']>;
|
||||
syncStage: CalendarChannelSyncStage;
|
||||
syncStageStartedAt?: Maybe<Scalars['DateTime']>;
|
||||
syncStatus: CalendarChannelSyncStatus;
|
||||
syncedAt?: Maybe<Scalars['DateTime']>;
|
||||
throttleFailureCount: Scalars['Float'];
|
||||
updatedAt: Scalars['DateTime'];
|
||||
visibility: CalendarChannelVisibility;
|
||||
};
|
||||
|
||||
export enum CalendarChannelContactAutoCreationPolicy {
|
||||
AS_ORGANIZER = 'AS_ORGANIZER',
|
||||
AS_PARTICIPANT = 'AS_PARTICIPANT',
|
||||
AS_PARTICIPANT_AND_ORGANIZER = 'AS_PARTICIPANT_AND_ORGANIZER',
|
||||
NONE = 'NONE'
|
||||
}
|
||||
|
||||
export enum CalendarChannelSyncStage {
|
||||
CALENDAR_EVENTS_IMPORT_ONGOING = 'CALENDAR_EVENTS_IMPORT_ONGOING',
|
||||
CALENDAR_EVENTS_IMPORT_PENDING = 'CALENDAR_EVENTS_IMPORT_PENDING',
|
||||
CALENDAR_EVENTS_IMPORT_SCHEDULED = 'CALENDAR_EVENTS_IMPORT_SCHEDULED',
|
||||
CALENDAR_EVENT_LIST_FETCH_ONGOING = 'CALENDAR_EVENT_LIST_FETCH_ONGOING',
|
||||
CALENDAR_EVENT_LIST_FETCH_PENDING = 'CALENDAR_EVENT_LIST_FETCH_PENDING',
|
||||
CALENDAR_EVENT_LIST_FETCH_SCHEDULED = 'CALENDAR_EVENT_LIST_FETCH_SCHEDULED',
|
||||
FAILED = 'FAILED',
|
||||
PENDING_CONFIGURATION = 'PENDING_CONFIGURATION'
|
||||
}
|
||||
|
||||
export enum CalendarChannelSyncStatus {
|
||||
ACTIVE = 'ACTIVE',
|
||||
FAILED_INSUFFICIENT_PERMISSIONS = 'FAILED_INSUFFICIENT_PERMISSIONS',
|
||||
FAILED_UNKNOWN = 'FAILED_UNKNOWN',
|
||||
NOT_SYNCED = 'NOT_SYNCED',
|
||||
ONGOING = 'ONGOING'
|
||||
}
|
||||
|
||||
export enum CalendarChannelVisibility {
|
||||
METADATA = 'METADATA',
|
||||
SHARE_EVERYTHING = 'SHARE_EVERYTHING'
|
||||
}
|
||||
|
||||
export type CalendarConfiguration = {
|
||||
__typename?: 'CalendarConfiguration';
|
||||
configurationType: WidgetConfigurationType;
|
||||
@@ -927,6 +977,25 @@ export type ConfigVariablesGroupData = {
|
||||
variables: Array<ConfigVariable>;
|
||||
};
|
||||
|
||||
export type ConnectedAccountDto = {
|
||||
__typename?: 'ConnectedAccountDTO';
|
||||
accessToken?: Maybe<Scalars['String']>;
|
||||
authFailedAt?: Maybe<Scalars['DateTime']>;
|
||||
connectionParameters?: Maybe<Scalars['JSON']>;
|
||||
createdAt: Scalars['DateTime'];
|
||||
handle: Scalars['String'];
|
||||
handleAliases?: Maybe<Array<Scalars['String']>>;
|
||||
id: Scalars['UUID'];
|
||||
lastCredentialsRefreshedAt?: Maybe<Scalars['DateTime']>;
|
||||
lastSignedInAt?: Maybe<Scalars['DateTime']>;
|
||||
oidcTokenClaims?: Maybe<Scalars['JSON']>;
|
||||
provider: Scalars['String'];
|
||||
refreshToken?: Maybe<Scalars['String']>;
|
||||
scopes?: Maybe<Array<Scalars['String']>>;
|
||||
updatedAt: Scalars['DateTime'];
|
||||
userWorkspaceId: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type ConnectedImapSmtpCaldavAccount = {
|
||||
__typename?: 'ConnectedImapSmtpCaldavAccount';
|
||||
accountOwnerId: Scalars['UUID'];
|
||||
@@ -1008,6 +1077,17 @@ export type CreateApprovedAccessDomainInput = {
|
||||
email: Scalars['String'];
|
||||
};
|
||||
|
||||
export type CreateCalendarChannelInput = {
|
||||
connectedAccountId: Scalars['UUID'];
|
||||
contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy;
|
||||
handle: Scalars['String'];
|
||||
id?: InputMaybe<Scalars['UUID']>;
|
||||
isContactAutoCreationEnabled: Scalars['Boolean'];
|
||||
isSyncEnabled: Scalars['Boolean'];
|
||||
syncStage: CalendarChannelSyncStage;
|
||||
visibility: CalendarChannelVisibility;
|
||||
};
|
||||
|
||||
export type CreateCommandMenuItemInput = {
|
||||
availabilityObjectMetadataId?: InputMaybe<Scalars['UUID']>;
|
||||
availabilityType?: InputMaybe<CommandMenuItemAvailabilityType>;
|
||||
@@ -1023,6 +1103,16 @@ export type CreateCommandMenuItemInput = {
|
||||
workflowVersionId?: InputMaybe<Scalars['UUID']>;
|
||||
};
|
||||
|
||||
export type CreateConnectedAccountInput = {
|
||||
accessToken?: InputMaybe<Scalars['String']>;
|
||||
handle: Scalars['String'];
|
||||
id?: InputMaybe<Scalars['UUID']>;
|
||||
provider: Scalars['String'];
|
||||
refreshToken?: InputMaybe<Scalars['String']>;
|
||||
scopes?: InputMaybe<Array<Scalars['String']>>;
|
||||
userWorkspaceId: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type CreateFieldInput = {
|
||||
defaultValue?: InputMaybe<Scalars['JSON']>;
|
||||
description?: InputMaybe<Scalars['String']>;
|
||||
@@ -1069,6 +1159,33 @@ export type CreateLogicFunctionFromSourceInput = {
|
||||
universalIdentifier?: InputMaybe<Scalars['UUID']>;
|
||||
};
|
||||
|
||||
export type CreateMessageChannelInput = {
|
||||
connectedAccountId: Scalars['UUID'];
|
||||
contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy;
|
||||
excludeGroupEmails: Scalars['Boolean'];
|
||||
excludeNonProfessionalEmails: Scalars['Boolean'];
|
||||
handle: Scalars['String'];
|
||||
id?: InputMaybe<Scalars['UUID']>;
|
||||
isContactAutoCreationEnabled: Scalars['Boolean'];
|
||||
isSyncEnabled: Scalars['Boolean'];
|
||||
messageFolderImportPolicy: MessageFolderImportPolicy;
|
||||
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction;
|
||||
syncStage: MessageChannelSyncStage;
|
||||
type: MessageChannelType;
|
||||
visibility: MessageChannelVisibility;
|
||||
};
|
||||
|
||||
export type CreateMessageFolderInput = {
|
||||
externalId?: InputMaybe<Scalars['String']>;
|
||||
id?: InputMaybe<Scalars['UUID']>;
|
||||
isSentFolder: Scalars['Boolean'];
|
||||
isSynced: Scalars['Boolean'];
|
||||
messageChannelId: Scalars['UUID'];
|
||||
name?: InputMaybe<Scalars['String']>;
|
||||
parentFolderId?: InputMaybe<Scalars['UUID']>;
|
||||
pendingSyncAction: MessageFolderPendingSyncAction;
|
||||
};
|
||||
|
||||
export type CreateNavigationMenuItemInput = {
|
||||
color?: InputMaybe<Scalars['String']>;
|
||||
folderId?: InputMaybe<Scalars['UUID']>;
|
||||
@@ -1618,6 +1735,7 @@ export enum FeatureFlagKey {
|
||||
IS_APPLICATION_ENABLED = 'IS_APPLICATION_ENABLED',
|
||||
IS_ATTACHMENT_MIGRATED = 'IS_ATTACHMENT_MIGRATED',
|
||||
IS_COMMAND_MENU_ITEM_ENABLED = 'IS_COMMAND_MENU_ITEM_ENABLED',
|
||||
IS_CONNECTED_ACCOUNT_MIGRATED = 'IS_CONNECTED_ACCOUNT_MIGRATED',
|
||||
IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED',
|
||||
IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED = 'IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED',
|
||||
IS_DRAFT_EMAIL_ENABLED = 'IS_DRAFT_EMAIL_ENABLED',
|
||||
@@ -2289,6 +2407,98 @@ export type MarketplaceAppRoleObjectPermission = {
|
||||
objectUniversalIdentifier: Scalars['String'];
|
||||
};
|
||||
|
||||
export type MessageChannel = {
|
||||
__typename?: 'MessageChannel';
|
||||
connectedAccountId: Scalars['UUID'];
|
||||
contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy;
|
||||
createdAt: Scalars['DateTime'];
|
||||
excludeGroupEmails: Scalars['Boolean'];
|
||||
excludeNonProfessionalEmails: Scalars['Boolean'];
|
||||
handle: Scalars['String'];
|
||||
id: Scalars['UUID'];
|
||||
isContactAutoCreationEnabled: Scalars['Boolean'];
|
||||
isSyncEnabled: Scalars['Boolean'];
|
||||
messageFolderImportPolicy: MessageFolderImportPolicy;
|
||||
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction;
|
||||
syncCursor?: Maybe<Scalars['String']>;
|
||||
syncStage: MessageChannelSyncStage;
|
||||
syncStageStartedAt?: Maybe<Scalars['DateTime']>;
|
||||
syncStatus: MessageChannelSyncStatus;
|
||||
syncedAt?: Maybe<Scalars['DateTime']>;
|
||||
throttleFailureCount: Scalars['Float'];
|
||||
throttleRetryAfter?: Maybe<Scalars['DateTime']>;
|
||||
type: MessageChannelType;
|
||||
updatedAt: Scalars['DateTime'];
|
||||
visibility: MessageChannelVisibility;
|
||||
};
|
||||
|
||||
export enum MessageChannelContactAutoCreationPolicy {
|
||||
NONE = 'NONE',
|
||||
SENT = 'SENT',
|
||||
SENT_AND_RECEIVED = 'SENT_AND_RECEIVED'
|
||||
}
|
||||
|
||||
export enum MessageChannelPendingGroupEmailsAction {
|
||||
GROUP_EMAILS_DELETION = 'GROUP_EMAILS_DELETION',
|
||||
GROUP_EMAILS_IMPORT = 'GROUP_EMAILS_IMPORT',
|
||||
NONE = 'NONE'
|
||||
}
|
||||
|
||||
export enum MessageChannelSyncStage {
|
||||
FAILED = 'FAILED',
|
||||
MESSAGES_IMPORT_ONGOING = 'MESSAGES_IMPORT_ONGOING',
|
||||
MESSAGES_IMPORT_PENDING = 'MESSAGES_IMPORT_PENDING',
|
||||
MESSAGES_IMPORT_SCHEDULED = 'MESSAGES_IMPORT_SCHEDULED',
|
||||
MESSAGE_LIST_FETCH_ONGOING = 'MESSAGE_LIST_FETCH_ONGOING',
|
||||
MESSAGE_LIST_FETCH_PENDING = 'MESSAGE_LIST_FETCH_PENDING',
|
||||
MESSAGE_LIST_FETCH_SCHEDULED = 'MESSAGE_LIST_FETCH_SCHEDULED',
|
||||
PENDING_CONFIGURATION = 'PENDING_CONFIGURATION'
|
||||
}
|
||||
|
||||
export enum MessageChannelSyncStatus {
|
||||
ACTIVE = 'ACTIVE',
|
||||
FAILED_INSUFFICIENT_PERMISSIONS = 'FAILED_INSUFFICIENT_PERMISSIONS',
|
||||
FAILED_UNKNOWN = 'FAILED_UNKNOWN',
|
||||
NOT_SYNCED = 'NOT_SYNCED',
|
||||
ONGOING = 'ONGOING'
|
||||
}
|
||||
|
||||
export enum MessageChannelType {
|
||||
EMAIL = 'EMAIL',
|
||||
SMS = 'SMS'
|
||||
}
|
||||
|
||||
export enum MessageChannelVisibility {
|
||||
METADATA = 'METADATA',
|
||||
SHARE_EVERYTHING = 'SHARE_EVERYTHING',
|
||||
SUBJECT = 'SUBJECT'
|
||||
}
|
||||
|
||||
export type MessageFolder = {
|
||||
__typename?: 'MessageFolder';
|
||||
createdAt: Scalars['DateTime'];
|
||||
externalId?: Maybe<Scalars['String']>;
|
||||
id: Scalars['UUID'];
|
||||
isSentFolder: Scalars['Boolean'];
|
||||
isSynced: Scalars['Boolean'];
|
||||
messageChannelId: Scalars['UUID'];
|
||||
name?: Maybe<Scalars['String']>;
|
||||
parentFolderId?: Maybe<Scalars['UUID']>;
|
||||
pendingSyncAction: MessageFolderPendingSyncAction;
|
||||
syncCursor?: Maybe<Scalars['String']>;
|
||||
updatedAt: Scalars['DateTime'];
|
||||
};
|
||||
|
||||
export enum MessageFolderImportPolicy {
|
||||
ALL_FOLDERS = 'ALL_FOLDERS',
|
||||
SELECTED_FOLDERS = 'SELECTED_FOLDERS'
|
||||
}
|
||||
|
||||
export enum MessageFolderPendingSyncAction {
|
||||
FOLDER_DELETION = 'FOLDER_DELETION',
|
||||
NONE = 'NONE'
|
||||
}
|
||||
|
||||
export type MetadataEvent = {
|
||||
__typename?: 'MetadataEvent';
|
||||
metadataName: Scalars['String'];
|
||||
@@ -2361,8 +2571,10 @@ export type Mutation = {
|
||||
createApplicationRegistration: CreateApplicationRegistration;
|
||||
createApplicationRegistrationVariable: ApplicationRegistrationVariable;
|
||||
createApprovedAccessDomain: ApprovedAccessDomain;
|
||||
createCalendarChannel: CalendarChannel;
|
||||
createChatThread: AgentChatThread;
|
||||
createCommandMenuItem: CommandMenuItem;
|
||||
createConnectedAccount: ConnectedAccountDto;
|
||||
createDatabaseConfigVariable: Scalars['Boolean'];
|
||||
createDevelopmentApplication: DevelopmentApplication;
|
||||
createEmailingDomain: EmailingDomain;
|
||||
@@ -2370,6 +2582,8 @@ export type Mutation = {
|
||||
createManyViewFieldGroups: Array<ViewFieldGroup>;
|
||||
createManyViewFields: Array<ViewField>;
|
||||
createManyViewGroups: Array<ViewGroup>;
|
||||
createMessageChannel: MessageChannel;
|
||||
createMessageFolder: MessageFolder;
|
||||
createNavigationMenuItem: NavigationMenuItem;
|
||||
createOIDCIdentityProvider: SetupSso;
|
||||
createObjectEvent: Analytics;
|
||||
@@ -2397,12 +2611,16 @@ export type Mutation = {
|
||||
deleteApplicationRegistration: Scalars['Boolean'];
|
||||
deleteApplicationRegistrationVariable: Scalars['Boolean'];
|
||||
deleteApprovedAccessDomain: Scalars['Boolean'];
|
||||
deleteCalendarChannel: CalendarChannel;
|
||||
deleteCommandMenuItem: CommandMenuItem;
|
||||
deleteConnectedAccount: ConnectedAccountDto;
|
||||
deleteCurrentWorkspace: Workspace;
|
||||
deleteDatabaseConfigVariable: Scalars['Boolean'];
|
||||
deleteEmailingDomain: Scalars['Boolean'];
|
||||
deleteFrontComponent: FrontComponent;
|
||||
deleteJobs: DeleteJobsResponse;
|
||||
deleteMessageChannel: MessageChannel;
|
||||
deleteMessageFolder: MessageFolder;
|
||||
deleteNavigationMenuItem: NavigationMenuItem;
|
||||
deleteOneAgent: Agent;
|
||||
deleteOneField: Field;
|
||||
@@ -2487,10 +2705,14 @@ export type Mutation = {
|
||||
updateApiKey?: Maybe<ApiKey>;
|
||||
updateApplicationRegistration: ApplicationRegistration;
|
||||
updateApplicationRegistrationVariable: ApplicationRegistrationVariable;
|
||||
updateCalendarChannel: CalendarChannel;
|
||||
updateCommandMenuItem: CommandMenuItem;
|
||||
updateConnectedAccount: ConnectedAccountDto;
|
||||
updateDatabaseConfigVariable: Scalars['Boolean'];
|
||||
updateFrontComponent: FrontComponent;
|
||||
updateLabPublicFeatureFlag: FeatureFlag;
|
||||
updateMessageChannel: MessageChannel;
|
||||
updateMessageFolder: MessageFolder;
|
||||
updateNavigationMenuItem: NavigationMenuItem;
|
||||
updateOneAgent: Agent;
|
||||
updateOneApplicationVariable: Scalars['Boolean'];
|
||||
@@ -2608,11 +2830,21 @@ export type MutationCreateApprovedAccessDomainArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateCalendarChannelArgs = {
|
||||
input: CreateCalendarChannelInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateCommandMenuItemArgs = {
|
||||
input: CreateCommandMenuItemInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateConnectedAccountArgs = {
|
||||
input: CreateConnectedAccountInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateDatabaseConfigVariableArgs = {
|
||||
key: Scalars['String'];
|
||||
value: Scalars['JSON'];
|
||||
@@ -2651,6 +2883,16 @@ export type MutationCreateManyViewGroupsArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateMessageChannelArgs = {
|
||||
input: CreateMessageChannelInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateMessageFolderArgs = {
|
||||
input: CreateMessageFolderInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateNavigationMenuItemArgs = {
|
||||
input: CreateNavigationMenuItemInput;
|
||||
};
|
||||
@@ -2789,11 +3031,21 @@ export type MutationDeleteApprovedAccessDomainArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteCalendarChannelArgs = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteCommandMenuItemArgs = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteConnectedAccountArgs = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteDatabaseConfigVariableArgs = {
|
||||
key: Scalars['String'];
|
||||
};
|
||||
@@ -2815,6 +3067,16 @@ export type MutationDeleteJobsArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteMessageChannelArgs = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteMessageFolderArgs = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteNavigationMenuItemArgs = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
@@ -3217,11 +3479,21 @@ export type MutationUpdateApplicationRegistrationVariableArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateCalendarChannelArgs = {
|
||||
input: UpdateCalendarChannelInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateCommandMenuItemArgs = {
|
||||
input: UpdateCommandMenuItemInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateConnectedAccountArgs = {
|
||||
input: UpdateConnectedAccountInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateDatabaseConfigVariableArgs = {
|
||||
key: Scalars['String'];
|
||||
value: Scalars['JSON'];
|
||||
@@ -3238,6 +3510,16 @@ export type MutationUpdateLabPublicFeatureFlagArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateMessageChannelArgs = {
|
||||
input: UpdateMessageChannelInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateMessageFolderArgs = {
|
||||
input: UpdateMessageFolderInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateNavigationMenuItemArgs = {
|
||||
input: UpdateOneNavigationMenuItemInput;
|
||||
};
|
||||
@@ -3939,6 +4221,8 @@ export type Query = {
|
||||
applicationRegistrationTarballUrl?: Maybe<Scalars['String']>;
|
||||
barChartData: BarChartData;
|
||||
billingPortalSession: BillingSession;
|
||||
calendarChannel?: Maybe<CalendarChannel>;
|
||||
calendarChannels: Array<CalendarChannel>;
|
||||
chatMessages: Array<AgentMessage>;
|
||||
chatThread: AgentChatThread;
|
||||
chatThreads: AgentChatThreadConnection;
|
||||
@@ -3946,6 +4230,8 @@ export type Query = {
|
||||
checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValid;
|
||||
commandMenuItem?: Maybe<CommandMenuItem>;
|
||||
commandMenuItems: Array<CommandMenuItem>;
|
||||
connectedAccount?: Maybe<ConnectedAccountDto>;
|
||||
connectedAccounts: Array<ConnectedAccountDto>;
|
||||
currentUser: User;
|
||||
currentWorkspace: Workspace;
|
||||
enterpriseCheckoutSession?: Maybe<Scalars['String']>;
|
||||
@@ -4020,6 +4306,10 @@ export type Query = {
|
||||
indexMetadatas: IndexConnection;
|
||||
lineChartData: LineChartData;
|
||||
listPlans: Array<BillingPlan>;
|
||||
messageChannel?: Maybe<MessageChannel>;
|
||||
messageChannels: Array<MessageChannel>;
|
||||
messageFolder?: Maybe<MessageFolder>;
|
||||
messageFolders: Array<MessageFolder>;
|
||||
minimalMetadata: MinimalMetadata;
|
||||
navigationMenuItem?: Maybe<NavigationMenuItem>;
|
||||
navigationMenuItems: Array<NavigationMenuItem>;
|
||||
@@ -4061,6 +4351,16 @@ export type QueryBillingPortalSessionArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryCalendarChannelArgs = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryCalendarChannelsArgs = {
|
||||
connectedAccountId?: InputMaybe<Scalars['UUID']>;
|
||||
};
|
||||
|
||||
|
||||
export type QueryChatMessagesArgs = {
|
||||
threadId: Scalars['UUID'];
|
||||
};
|
||||
@@ -4094,6 +4394,11 @@ export type QueryCommandMenuItemArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryConnectedAccountArgs = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryEnterpriseCheckoutSessionArgs = {
|
||||
billingInterval?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
@@ -4357,6 +4662,26 @@ export type QueryLineChartDataArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryMessageChannelArgs = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryMessageChannelsArgs = {
|
||||
connectedAccountId?: InputMaybe<Scalars['UUID']>;
|
||||
};
|
||||
|
||||
|
||||
export type QueryMessageFolderArgs = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryMessageFoldersArgs = {
|
||||
messageChannelId?: InputMaybe<Scalars['UUID']>;
|
||||
};
|
||||
|
||||
|
||||
export type QueryNavigationMenuItemArgs = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
@@ -4871,6 +5196,18 @@ export type UpdateApplicationRegistrationVariablePayload = {
|
||||
value?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type UpdateCalendarChannelInput = {
|
||||
id: Scalars['UUID'];
|
||||
update: UpdateCalendarChannelInputUpdates;
|
||||
};
|
||||
|
||||
export type UpdateCalendarChannelInputUpdates = {
|
||||
contactAutoCreationPolicy?: InputMaybe<CalendarChannelContactAutoCreationPolicy>;
|
||||
isContactAutoCreationEnabled?: InputMaybe<Scalars['Boolean']>;
|
||||
isSyncEnabled?: InputMaybe<Scalars['Boolean']>;
|
||||
visibility?: InputMaybe<CalendarChannelVisibility>;
|
||||
};
|
||||
|
||||
export type UpdateCommandMenuItemInput = {
|
||||
availabilityObjectMetadataId?: InputMaybe<Scalars['UUID']>;
|
||||
availabilityType?: InputMaybe<CommandMenuItemAvailabilityType>;
|
||||
@@ -4884,6 +5221,18 @@ export type UpdateCommandMenuItemInput = {
|
||||
shortLabel?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type UpdateConnectedAccountInput = {
|
||||
id: Scalars['UUID'];
|
||||
update: UpdateConnectedAccountInputUpdates;
|
||||
};
|
||||
|
||||
export type UpdateConnectedAccountInputUpdates = {
|
||||
accessToken?: InputMaybe<Scalars['String']>;
|
||||
handleAliases?: InputMaybe<Array<Scalars['String']>>;
|
||||
refreshToken?: InputMaybe<Scalars['String']>;
|
||||
scopes?: InputMaybe<Array<Scalars['String']>>;
|
||||
};
|
||||
|
||||
export type UpdateFieldInput = {
|
||||
defaultValue?: InputMaybe<Scalars['JSON']>;
|
||||
description?: InputMaybe<Scalars['String']>;
|
||||
@@ -4940,6 +5289,33 @@ export type UpdateLogicFunctionFromSourceInputUpdates = {
|
||||
toolInputSchema?: InputMaybe<Scalars['JSON']>;
|
||||
};
|
||||
|
||||
export type UpdateMessageChannelInput = {
|
||||
id: Scalars['UUID'];
|
||||
update: UpdateMessageChannelInputUpdates;
|
||||
};
|
||||
|
||||
export type UpdateMessageChannelInputUpdates = {
|
||||
contactAutoCreationPolicy?: InputMaybe<MessageChannelContactAutoCreationPolicy>;
|
||||
excludeGroupEmails?: InputMaybe<Scalars['Boolean']>;
|
||||
excludeNonProfessionalEmails?: InputMaybe<Scalars['Boolean']>;
|
||||
isContactAutoCreationEnabled?: InputMaybe<Scalars['Boolean']>;
|
||||
isSyncEnabled?: InputMaybe<Scalars['Boolean']>;
|
||||
messageFolderImportPolicy?: InputMaybe<MessageFolderImportPolicy>;
|
||||
visibility?: InputMaybe<MessageChannelVisibility>;
|
||||
};
|
||||
|
||||
export type UpdateMessageFolderInput = {
|
||||
id: Scalars['UUID'];
|
||||
update: UpdateMessageFolderInputUpdates;
|
||||
};
|
||||
|
||||
export type UpdateMessageFolderInputUpdates = {
|
||||
isSynced?: InputMaybe<Scalars['Boolean']>;
|
||||
name?: InputMaybe<Scalars['String']>;
|
||||
pendingSyncAction?: InputMaybe<MessageFolderPendingSyncAction>;
|
||||
syncCursor?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type UpdateNavigationMenuItemInput = {
|
||||
color?: InputMaybe<Scalars['String']>;
|
||||
folderId?: InputMaybe<Scalars['UUID']>;
|
||||
|
||||
@@ -1549,6 +1549,7 @@ enum FeatureFlagKey {
|
||||
IS_DRAFT_EMAIL_ENABLED
|
||||
IS_RICH_TEXT_V1_MIGRATED
|
||||
IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED
|
||||
IS_CONNECTED_ACCOUNT_MIGRATED
|
||||
}
|
||||
|
||||
type SSOIdentityProvider {
|
||||
@@ -2204,6 +2205,163 @@ type AgentTurn {
|
||||
createdAt: DateTime!
|
||||
}
|
||||
|
||||
type CalendarChannel {
|
||||
id: UUID!
|
||||
handle: String!
|
||||
syncStatus: CalendarChannelSyncStatus!
|
||||
syncStage: CalendarChannelSyncStage!
|
||||
visibility: CalendarChannelVisibility!
|
||||
isContactAutoCreationEnabled: Boolean!
|
||||
contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy!
|
||||
isSyncEnabled: Boolean!
|
||||
syncCursor: String
|
||||
syncedAt: DateTime
|
||||
syncStageStartedAt: DateTime
|
||||
throttleFailureCount: Float!
|
||||
connectedAccountId: UUID!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
enum CalendarChannelSyncStatus {
|
||||
NOT_SYNCED
|
||||
ONGOING
|
||||
ACTIVE
|
||||
FAILED_INSUFFICIENT_PERMISSIONS
|
||||
FAILED_UNKNOWN
|
||||
}
|
||||
|
||||
enum CalendarChannelSyncStage {
|
||||
PENDING_CONFIGURATION
|
||||
CALENDAR_EVENT_LIST_FETCH_PENDING
|
||||
CALENDAR_EVENT_LIST_FETCH_SCHEDULED
|
||||
CALENDAR_EVENT_LIST_FETCH_ONGOING
|
||||
CALENDAR_EVENTS_IMPORT_PENDING
|
||||
CALENDAR_EVENTS_IMPORT_SCHEDULED
|
||||
CALENDAR_EVENTS_IMPORT_ONGOING
|
||||
FAILED
|
||||
}
|
||||
|
||||
enum CalendarChannelVisibility {
|
||||
METADATA
|
||||
SHARE_EVERYTHING
|
||||
}
|
||||
|
||||
enum CalendarChannelContactAutoCreationPolicy {
|
||||
AS_PARTICIPANT_AND_ORGANIZER
|
||||
AS_PARTICIPANT
|
||||
AS_ORGANIZER
|
||||
NONE
|
||||
}
|
||||
|
||||
type ConnectedAccountDTO {
|
||||
id: UUID!
|
||||
handle: String!
|
||||
provider: String!
|
||||
accessToken: String
|
||||
refreshToken: String
|
||||
lastCredentialsRefreshedAt: DateTime
|
||||
authFailedAt: DateTime
|
||||
handleAliases: [String!]
|
||||
scopes: [String!]
|
||||
connectionParameters: JSON
|
||||
lastSignedInAt: DateTime
|
||||
oidcTokenClaims: JSON
|
||||
userWorkspaceId: UUID!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
type MessageChannel {
|
||||
id: UUID!
|
||||
visibility: MessageChannelVisibility!
|
||||
handle: String!
|
||||
type: MessageChannelType!
|
||||
isContactAutoCreationEnabled: Boolean!
|
||||
contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy!
|
||||
messageFolderImportPolicy: MessageFolderImportPolicy!
|
||||
excludeNonProfessionalEmails: Boolean!
|
||||
excludeGroupEmails: Boolean!
|
||||
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction!
|
||||
isSyncEnabled: Boolean!
|
||||
syncCursor: String
|
||||
syncedAt: DateTime
|
||||
syncStatus: MessageChannelSyncStatus!
|
||||
syncStage: MessageChannelSyncStage!
|
||||
syncStageStartedAt: DateTime
|
||||
throttleFailureCount: Float!
|
||||
throttleRetryAfter: DateTime
|
||||
connectedAccountId: UUID!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
enum MessageChannelVisibility {
|
||||
METADATA
|
||||
SUBJECT
|
||||
SHARE_EVERYTHING
|
||||
}
|
||||
|
||||
enum MessageChannelType {
|
||||
EMAIL
|
||||
SMS
|
||||
}
|
||||
|
||||
enum MessageChannelContactAutoCreationPolicy {
|
||||
SENT_AND_RECEIVED
|
||||
SENT
|
||||
NONE
|
||||
}
|
||||
|
||||
enum MessageFolderImportPolicy {
|
||||
ALL_FOLDERS
|
||||
SELECTED_FOLDERS
|
||||
}
|
||||
|
||||
enum MessageChannelPendingGroupEmailsAction {
|
||||
GROUP_EMAILS_DELETION
|
||||
GROUP_EMAILS_IMPORT
|
||||
NONE
|
||||
}
|
||||
|
||||
enum MessageChannelSyncStatus {
|
||||
NOT_SYNCED
|
||||
ONGOING
|
||||
ACTIVE
|
||||
FAILED_INSUFFICIENT_PERMISSIONS
|
||||
FAILED_UNKNOWN
|
||||
}
|
||||
|
||||
enum MessageChannelSyncStage {
|
||||
PENDING_CONFIGURATION
|
||||
MESSAGE_LIST_FETCH_PENDING
|
||||
MESSAGE_LIST_FETCH_SCHEDULED
|
||||
MESSAGE_LIST_FETCH_ONGOING
|
||||
MESSAGES_IMPORT_PENDING
|
||||
MESSAGES_IMPORT_SCHEDULED
|
||||
MESSAGES_IMPORT_ONGOING
|
||||
FAILED
|
||||
}
|
||||
|
||||
type MessageFolder {
|
||||
id: UUID!
|
||||
name: String
|
||||
syncCursor: String
|
||||
isSentFolder: Boolean!
|
||||
isSynced: Boolean!
|
||||
parentFolderId: UUID
|
||||
externalId: String
|
||||
pendingSyncAction: MessageFolderPendingSyncAction!
|
||||
messageChannelId: UUID!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
enum MessageFolderPendingSyncAction {
|
||||
FOLDER_DELETION
|
||||
NONE
|
||||
}
|
||||
|
||||
type CollectionHash {
|
||||
collectionName: AllMetadataName!
|
||||
hash: String!
|
||||
@@ -3010,6 +3168,14 @@ type Query {
|
||||
findWorkspaceFromInviteHash(inviteHash: String!): Workspace!
|
||||
validatePasswordResetToken(passwordResetToken: String!): ValidatePasswordResetToken!
|
||||
getSSOIdentityProviders: [FindAvailableSSOIDP!]!
|
||||
messageFolders(messageChannelId: UUID): [MessageFolder!]!
|
||||
messageFolder(id: UUID!): MessageFolder
|
||||
calendarChannels(connectedAccountId: UUID): [CalendarChannel!]!
|
||||
calendarChannel(id: UUID!): CalendarChannel
|
||||
messageChannels(connectedAccountId: UUID): [MessageChannel!]!
|
||||
messageChannel(id: UUID!): MessageChannel
|
||||
connectedAccounts: [ConnectedAccountDTO!]!
|
||||
connectedAccount(id: UUID!): ConnectedAccountDTO
|
||||
webhooks: [Webhook!]!
|
||||
webhook(id: UUID!): Webhook
|
||||
minimalMetadata: MinimalMetadata!
|
||||
@@ -3307,6 +3473,18 @@ type Mutation {
|
||||
createSAMLIdentityProvider(input: SetupSAMLSsoInput!): SetupSso!
|
||||
deleteSSOIdentityProvider(input: DeleteSsoInput!): DeleteSso!
|
||||
editSSOIdentityProvider(input: EditSsoInput!): EditSso!
|
||||
createMessageFolder(input: CreateMessageFolderInput!): MessageFolder!
|
||||
updateMessageFolder(input: UpdateMessageFolderInput!): MessageFolder!
|
||||
deleteMessageFolder(id: UUID!): MessageFolder!
|
||||
createCalendarChannel(input: CreateCalendarChannelInput!): CalendarChannel!
|
||||
updateCalendarChannel(input: UpdateCalendarChannelInput!): CalendarChannel!
|
||||
deleteCalendarChannel(id: UUID!): CalendarChannel!
|
||||
createMessageChannel(input: CreateMessageChannelInput!): MessageChannel!
|
||||
updateMessageChannel(input: UpdateMessageChannelInput!): MessageChannel!
|
||||
deleteMessageChannel(id: UUID!): MessageChannel!
|
||||
createConnectedAccount(input: CreateConnectedAccountInput!): ConnectedAccountDTO!
|
||||
updateConnectedAccount(input: UpdateConnectedAccountInput!): ConnectedAccountDTO!
|
||||
deleteConnectedAccount(id: UUID!): ConnectedAccountDTO!
|
||||
createWebhook(input: CreateWebhookInput!): Webhook!
|
||||
updateWebhook(input: UpdateWebhookInput!): Webhook!
|
||||
deleteWebhook(id: UUID!): Webhook!
|
||||
@@ -4203,6 +4381,105 @@ input EditSsoInput {
|
||||
status: SSOIdentityProviderStatus!
|
||||
}
|
||||
|
||||
input CreateMessageFolderInput {
|
||||
id: UUID
|
||||
name: String
|
||||
isSentFolder: Boolean!
|
||||
isSynced: Boolean!
|
||||
externalId: String
|
||||
pendingSyncAction: MessageFolderPendingSyncAction!
|
||||
messageChannelId: UUID!
|
||||
parentFolderId: UUID
|
||||
}
|
||||
|
||||
input UpdateMessageFolderInput {
|
||||
id: UUID!
|
||||
update: UpdateMessageFolderInputUpdates!
|
||||
}
|
||||
|
||||
input UpdateMessageFolderInputUpdates {
|
||||
name: String
|
||||
syncCursor: String
|
||||
isSynced: Boolean
|
||||
pendingSyncAction: MessageFolderPendingSyncAction
|
||||
}
|
||||
|
||||
input CreateCalendarChannelInput {
|
||||
id: UUID
|
||||
handle: String!
|
||||
visibility: CalendarChannelVisibility!
|
||||
syncStage: CalendarChannelSyncStage!
|
||||
connectedAccountId: UUID!
|
||||
isContactAutoCreationEnabled: Boolean!
|
||||
contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy!
|
||||
isSyncEnabled: Boolean!
|
||||
}
|
||||
|
||||
input UpdateCalendarChannelInput {
|
||||
id: UUID!
|
||||
update: UpdateCalendarChannelInputUpdates!
|
||||
}
|
||||
|
||||
input UpdateCalendarChannelInputUpdates {
|
||||
visibility: CalendarChannelVisibility
|
||||
isContactAutoCreationEnabled: Boolean
|
||||
contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy
|
||||
isSyncEnabled: Boolean
|
||||
}
|
||||
|
||||
input CreateMessageChannelInput {
|
||||
id: UUID
|
||||
handle: String!
|
||||
visibility: MessageChannelVisibility!
|
||||
type: MessageChannelType!
|
||||
syncStage: MessageChannelSyncStage!
|
||||
connectedAccountId: UUID!
|
||||
isContactAutoCreationEnabled: Boolean!
|
||||
contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy!
|
||||
messageFolderImportPolicy: MessageFolderImportPolicy!
|
||||
excludeNonProfessionalEmails: Boolean!
|
||||
excludeGroupEmails: Boolean!
|
||||
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction!
|
||||
isSyncEnabled: Boolean!
|
||||
}
|
||||
|
||||
input UpdateMessageChannelInput {
|
||||
id: UUID!
|
||||
update: UpdateMessageChannelInputUpdates!
|
||||
}
|
||||
|
||||
input UpdateMessageChannelInputUpdates {
|
||||
visibility: MessageChannelVisibility
|
||||
isContactAutoCreationEnabled: Boolean
|
||||
contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy
|
||||
messageFolderImportPolicy: MessageFolderImportPolicy
|
||||
isSyncEnabled: Boolean
|
||||
excludeNonProfessionalEmails: Boolean
|
||||
excludeGroupEmails: Boolean
|
||||
}
|
||||
|
||||
input CreateConnectedAccountInput {
|
||||
id: UUID
|
||||
handle: String!
|
||||
provider: String!
|
||||
accessToken: String
|
||||
refreshToken: String
|
||||
scopes: [String!]
|
||||
userWorkspaceId: UUID!
|
||||
}
|
||||
|
||||
input UpdateConnectedAccountInput {
|
||||
id: UUID!
|
||||
update: UpdateConnectedAccountInputUpdates!
|
||||
}
|
||||
|
||||
input UpdateConnectedAccountInputUpdates {
|
||||
accessToken: String
|
||||
refreshToken: String
|
||||
handleAliases: [String!]
|
||||
scopes: [String!]
|
||||
}
|
||||
|
||||
input CreateWebhookInput {
|
||||
id: UUID
|
||||
targetUrl: String!
|
||||
|
||||
@@ -1252,7 +1252,7 @@ export interface FeatureFlag {
|
||||
__typename: 'FeatureFlag'
|
||||
}
|
||||
|
||||
export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_AI_ENABLED' | 'IS_APPLICATION_ENABLED' | 'IS_MARKETPLACE_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAILING_DOMAIN_ENABLED' | 'IS_DASHBOARD_V2_ENABLED' | 'IS_ATTACHMENT_MIGRATED' | 'IS_NOTE_TARGET_MIGRATED' | 'IS_TASK_TARGET_MIGRATED' | 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_COMMAND_MENU_ITEM_ENABLED' | 'IS_NAVIGATION_MENU_ITEM_ENABLED' | 'IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED' | 'IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED' | 'IS_DRAFT_EMAIL_ENABLED' | 'IS_RICH_TEXT_V1_MIGRATED' | 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED'
|
||||
export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_AI_ENABLED' | 'IS_APPLICATION_ENABLED' | 'IS_MARKETPLACE_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAILING_DOMAIN_ENABLED' | 'IS_DASHBOARD_V2_ENABLED' | 'IS_ATTACHMENT_MIGRATED' | 'IS_NOTE_TARGET_MIGRATED' | 'IS_TASK_TARGET_MIGRATED' | 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_COMMAND_MENU_ITEM_ENABLED' | 'IS_NAVIGATION_MENU_ITEM_ENABLED' | 'IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED' | 'IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED' | 'IS_DRAFT_EMAIL_ENABLED' | 'IS_RICH_TEXT_V1_MIGRATED' | 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' | 'IS_CONNECTED_ACCOUNT_MIGRATED'
|
||||
|
||||
export interface SSOIdentityProvider {
|
||||
id: Scalars['UUID']
|
||||
@@ -1877,6 +1877,108 @@ export interface AgentTurn {
|
||||
__typename: 'AgentTurn'
|
||||
}
|
||||
|
||||
export interface CalendarChannel {
|
||||
id: Scalars['UUID']
|
||||
handle: Scalars['String']
|
||||
syncStatus: CalendarChannelSyncStatus
|
||||
syncStage: CalendarChannelSyncStage
|
||||
visibility: CalendarChannelVisibility
|
||||
isContactAutoCreationEnabled: Scalars['Boolean']
|
||||
contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy
|
||||
isSyncEnabled: Scalars['Boolean']
|
||||
syncCursor?: Scalars['String']
|
||||
syncedAt?: Scalars['DateTime']
|
||||
syncStageStartedAt?: Scalars['DateTime']
|
||||
throttleFailureCount: Scalars['Float']
|
||||
connectedAccountId: Scalars['UUID']
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
__typename: 'CalendarChannel'
|
||||
}
|
||||
|
||||
export type CalendarChannelSyncStatus = 'NOT_SYNCED' | 'ONGOING' | 'ACTIVE' | 'FAILED_INSUFFICIENT_PERMISSIONS' | 'FAILED_UNKNOWN'
|
||||
|
||||
export type CalendarChannelSyncStage = 'PENDING_CONFIGURATION' | 'CALENDAR_EVENT_LIST_FETCH_PENDING' | 'CALENDAR_EVENT_LIST_FETCH_SCHEDULED' | 'CALENDAR_EVENT_LIST_FETCH_ONGOING' | 'CALENDAR_EVENTS_IMPORT_PENDING' | 'CALENDAR_EVENTS_IMPORT_SCHEDULED' | 'CALENDAR_EVENTS_IMPORT_ONGOING' | 'FAILED'
|
||||
|
||||
export type CalendarChannelVisibility = 'METADATA' | 'SHARE_EVERYTHING'
|
||||
|
||||
export type CalendarChannelContactAutoCreationPolicy = 'AS_PARTICIPANT_AND_ORGANIZER' | 'AS_PARTICIPANT' | 'AS_ORGANIZER' | 'NONE'
|
||||
|
||||
export interface ConnectedAccountDTO {
|
||||
id: Scalars['UUID']
|
||||
handle: Scalars['String']
|
||||
provider: Scalars['String']
|
||||
accessToken?: Scalars['String']
|
||||
refreshToken?: Scalars['String']
|
||||
lastCredentialsRefreshedAt?: Scalars['DateTime']
|
||||
authFailedAt?: Scalars['DateTime']
|
||||
handleAliases?: Scalars['String'][]
|
||||
scopes?: Scalars['String'][]
|
||||
connectionParameters?: Scalars['JSON']
|
||||
lastSignedInAt?: Scalars['DateTime']
|
||||
oidcTokenClaims?: Scalars['JSON']
|
||||
userWorkspaceId: Scalars['UUID']
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
__typename: 'ConnectedAccountDTO'
|
||||
}
|
||||
|
||||
export interface MessageChannel {
|
||||
id: Scalars['UUID']
|
||||
visibility: MessageChannelVisibility
|
||||
handle: Scalars['String']
|
||||
type: MessageChannelType
|
||||
isContactAutoCreationEnabled: Scalars['Boolean']
|
||||
contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy
|
||||
messageFolderImportPolicy: MessageFolderImportPolicy
|
||||
excludeNonProfessionalEmails: Scalars['Boolean']
|
||||
excludeGroupEmails: Scalars['Boolean']
|
||||
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction
|
||||
isSyncEnabled: Scalars['Boolean']
|
||||
syncCursor?: Scalars['String']
|
||||
syncedAt?: Scalars['DateTime']
|
||||
syncStatus: MessageChannelSyncStatus
|
||||
syncStage: MessageChannelSyncStage
|
||||
syncStageStartedAt?: Scalars['DateTime']
|
||||
throttleFailureCount: Scalars['Float']
|
||||
throttleRetryAfter?: Scalars['DateTime']
|
||||
connectedAccountId: Scalars['UUID']
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
__typename: 'MessageChannel'
|
||||
}
|
||||
|
||||
export type MessageChannelVisibility = 'METADATA' | 'SUBJECT' | 'SHARE_EVERYTHING'
|
||||
|
||||
export type MessageChannelType = 'EMAIL' | 'SMS'
|
||||
|
||||
export type MessageChannelContactAutoCreationPolicy = 'SENT_AND_RECEIVED' | 'SENT' | 'NONE'
|
||||
|
||||
export type MessageFolderImportPolicy = 'ALL_FOLDERS' | 'SELECTED_FOLDERS'
|
||||
|
||||
export type MessageChannelPendingGroupEmailsAction = 'GROUP_EMAILS_DELETION' | 'GROUP_EMAILS_IMPORT' | 'NONE'
|
||||
|
||||
export type MessageChannelSyncStatus = 'NOT_SYNCED' | 'ONGOING' | 'ACTIVE' | 'FAILED_INSUFFICIENT_PERMISSIONS' | 'FAILED_UNKNOWN'
|
||||
|
||||
export type MessageChannelSyncStage = 'PENDING_CONFIGURATION' | 'MESSAGE_LIST_FETCH_PENDING' | 'MESSAGE_LIST_FETCH_SCHEDULED' | 'MESSAGE_LIST_FETCH_ONGOING' | 'MESSAGES_IMPORT_PENDING' | 'MESSAGES_IMPORT_SCHEDULED' | 'MESSAGES_IMPORT_ONGOING' | 'FAILED'
|
||||
|
||||
export interface MessageFolder {
|
||||
id: Scalars['UUID']
|
||||
name?: Scalars['String']
|
||||
syncCursor?: Scalars['String']
|
||||
isSentFolder: Scalars['Boolean']
|
||||
isSynced: Scalars['Boolean']
|
||||
parentFolderId?: Scalars['UUID']
|
||||
externalId?: Scalars['String']
|
||||
pendingSyncAction: MessageFolderPendingSyncAction
|
||||
messageChannelId: Scalars['UUID']
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
__typename: 'MessageFolder'
|
||||
}
|
||||
|
||||
export type MessageFolderPendingSyncAction = 'FOLDER_DELETION' | 'NONE'
|
||||
|
||||
export interface CollectionHash {
|
||||
collectionName: AllMetadataName
|
||||
hash: Scalars['String']
|
||||
@@ -2622,6 +2724,14 @@ export interface Query {
|
||||
findWorkspaceFromInviteHash: Workspace
|
||||
validatePasswordResetToken: ValidatePasswordResetToken
|
||||
getSSOIdentityProviders: FindAvailableSSOIDP[]
|
||||
messageFolders: MessageFolder[]
|
||||
messageFolder?: MessageFolder
|
||||
calendarChannels: CalendarChannel[]
|
||||
calendarChannel?: CalendarChannel
|
||||
messageChannels: MessageChannel[]
|
||||
messageChannel?: MessageChannel
|
||||
connectedAccounts: ConnectedAccountDTO[]
|
||||
connectedAccount?: ConnectedAccountDTO
|
||||
webhooks: Webhook[]
|
||||
webhook?: Webhook
|
||||
minimalMetadata: MinimalMetadata
|
||||
@@ -2818,6 +2928,18 @@ export interface Mutation {
|
||||
createSAMLIdentityProvider: SetupSso
|
||||
deleteSSOIdentityProvider: DeleteSso
|
||||
editSSOIdentityProvider: EditSso
|
||||
createMessageFolder: MessageFolder
|
||||
updateMessageFolder: MessageFolder
|
||||
deleteMessageFolder: MessageFolder
|
||||
createCalendarChannel: CalendarChannel
|
||||
updateCalendarChannel: CalendarChannel
|
||||
deleteCalendarChannel: CalendarChannel
|
||||
createMessageChannel: MessageChannel
|
||||
updateMessageChannel: MessageChannel
|
||||
deleteMessageChannel: MessageChannel
|
||||
createConnectedAccount: ConnectedAccountDTO
|
||||
updateConnectedAccount: ConnectedAccountDTO
|
||||
deleteConnectedAccount: ConnectedAccountDTO
|
||||
createWebhook: Webhook
|
||||
updateWebhook: Webhook
|
||||
deleteWebhook: Webhook
|
||||
@@ -4863,6 +4985,88 @@ export interface AgentTurnGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface CalendarChannelGenqlSelection{
|
||||
id?: boolean | number
|
||||
handle?: boolean | number
|
||||
syncStatus?: boolean | number
|
||||
syncStage?: boolean | number
|
||||
visibility?: boolean | number
|
||||
isContactAutoCreationEnabled?: boolean | number
|
||||
contactAutoCreationPolicy?: boolean | number
|
||||
isSyncEnabled?: boolean | number
|
||||
syncCursor?: boolean | number
|
||||
syncedAt?: boolean | number
|
||||
syncStageStartedAt?: boolean | number
|
||||
throttleFailureCount?: boolean | number
|
||||
connectedAccountId?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
updatedAt?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ConnectedAccountDTOGenqlSelection{
|
||||
id?: boolean | number
|
||||
handle?: boolean | number
|
||||
provider?: boolean | number
|
||||
accessToken?: boolean | number
|
||||
refreshToken?: boolean | number
|
||||
lastCredentialsRefreshedAt?: boolean | number
|
||||
authFailedAt?: boolean | number
|
||||
handleAliases?: boolean | number
|
||||
scopes?: boolean | number
|
||||
connectionParameters?: boolean | number
|
||||
lastSignedInAt?: boolean | number
|
||||
oidcTokenClaims?: boolean | number
|
||||
userWorkspaceId?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
updatedAt?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface MessageChannelGenqlSelection{
|
||||
id?: boolean | number
|
||||
visibility?: boolean | number
|
||||
handle?: boolean | number
|
||||
type?: boolean | number
|
||||
isContactAutoCreationEnabled?: boolean | number
|
||||
contactAutoCreationPolicy?: boolean | number
|
||||
messageFolderImportPolicy?: boolean | number
|
||||
excludeNonProfessionalEmails?: boolean | number
|
||||
excludeGroupEmails?: boolean | number
|
||||
pendingGroupEmailsAction?: boolean | number
|
||||
isSyncEnabled?: boolean | number
|
||||
syncCursor?: boolean | number
|
||||
syncedAt?: boolean | number
|
||||
syncStatus?: boolean | number
|
||||
syncStage?: boolean | number
|
||||
syncStageStartedAt?: boolean | number
|
||||
throttleFailureCount?: boolean | number
|
||||
throttleRetryAfter?: boolean | number
|
||||
connectedAccountId?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
updatedAt?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface MessageFolderGenqlSelection{
|
||||
id?: boolean | number
|
||||
name?: boolean | number
|
||||
syncCursor?: boolean | number
|
||||
isSentFolder?: boolean | number
|
||||
isSynced?: boolean | number
|
||||
parentFolderId?: boolean | number
|
||||
externalId?: boolean | number
|
||||
pendingSyncAction?: boolean | number
|
||||
messageChannelId?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
updatedAt?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface CollectionHashGenqlSelection{
|
||||
collectionName?: boolean | number
|
||||
hash?: boolean | number
|
||||
@@ -5670,6 +5874,14 @@ export interface QueryGenqlSelection{
|
||||
findWorkspaceFromInviteHash?: (WorkspaceGenqlSelection & { __args: {inviteHash: Scalars['String']} })
|
||||
validatePasswordResetToken?: (ValidatePasswordResetTokenGenqlSelection & { __args: {passwordResetToken: Scalars['String']} })
|
||||
getSSOIdentityProviders?: FindAvailableSSOIDPGenqlSelection
|
||||
messageFolders?: (MessageFolderGenqlSelection & { __args?: {messageChannelId?: (Scalars['UUID'] | null)} })
|
||||
messageFolder?: (MessageFolderGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
calendarChannels?: (CalendarChannelGenqlSelection & { __args?: {connectedAccountId?: (Scalars['UUID'] | null)} })
|
||||
calendarChannel?: (CalendarChannelGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
messageChannels?: (MessageChannelGenqlSelection & { __args?: {connectedAccountId?: (Scalars['UUID'] | null)} })
|
||||
messageChannel?: (MessageChannelGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
connectedAccounts?: ConnectedAccountDTOGenqlSelection
|
||||
connectedAccount?: (ConnectedAccountDTOGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
webhooks?: WebhookGenqlSelection
|
||||
webhook?: (WebhookGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
minimalMetadata?: MinimalMetadataGenqlSelection
|
||||
@@ -5891,6 +6103,18 @@ export interface MutationGenqlSelection{
|
||||
createSAMLIdentityProvider?: (SetupSsoGenqlSelection & { __args: {input: SetupSAMLSsoInput} })
|
||||
deleteSSOIdentityProvider?: (DeleteSsoGenqlSelection & { __args: {input: DeleteSsoInput} })
|
||||
editSSOIdentityProvider?: (EditSsoGenqlSelection & { __args: {input: EditSsoInput} })
|
||||
createMessageFolder?: (MessageFolderGenqlSelection & { __args: {input: CreateMessageFolderInput} })
|
||||
updateMessageFolder?: (MessageFolderGenqlSelection & { __args: {input: UpdateMessageFolderInput} })
|
||||
deleteMessageFolder?: (MessageFolderGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
createCalendarChannel?: (CalendarChannelGenqlSelection & { __args: {input: CreateCalendarChannelInput} })
|
||||
updateCalendarChannel?: (CalendarChannelGenqlSelection & { __args: {input: UpdateCalendarChannelInput} })
|
||||
deleteCalendarChannel?: (CalendarChannelGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
createMessageChannel?: (MessageChannelGenqlSelection & { __args: {input: CreateMessageChannelInput} })
|
||||
updateMessageChannel?: (MessageChannelGenqlSelection & { __args: {input: UpdateMessageChannelInput} })
|
||||
deleteMessageChannel?: (MessageChannelGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
createConnectedAccount?: (ConnectedAccountDTOGenqlSelection & { __args: {input: CreateConnectedAccountInput} })
|
||||
updateConnectedAccount?: (ConnectedAccountDTOGenqlSelection & { __args: {input: UpdateConnectedAccountInput} })
|
||||
deleteConnectedAccount?: (ConnectedAccountDTOGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
createWebhook?: (WebhookGenqlSelection & { __args: {input: CreateWebhookInput} })
|
||||
updateWebhook?: (WebhookGenqlSelection & { __args: {input: UpdateWebhookInput} })
|
||||
deleteWebhook?: (WebhookGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
@@ -6215,6 +6439,30 @@ export interface DeleteSsoInput {identityProviderId: Scalars['UUID']}
|
||||
|
||||
export interface EditSsoInput {id: Scalars['UUID'],status: SSOIdentityProviderStatus}
|
||||
|
||||
export interface CreateMessageFolderInput {id?: (Scalars['UUID'] | null),name?: (Scalars['String'] | null),isSentFolder: Scalars['Boolean'],isSynced: Scalars['Boolean'],externalId?: (Scalars['String'] | null),pendingSyncAction: MessageFolderPendingSyncAction,messageChannelId: Scalars['UUID'],parentFolderId?: (Scalars['UUID'] | null)}
|
||||
|
||||
export interface UpdateMessageFolderInput {id: Scalars['UUID'],update: UpdateMessageFolderInputUpdates}
|
||||
|
||||
export interface UpdateMessageFolderInputUpdates {name?: (Scalars['String'] | null),syncCursor?: (Scalars['String'] | null),isSynced?: (Scalars['Boolean'] | null),pendingSyncAction?: (MessageFolderPendingSyncAction | null)}
|
||||
|
||||
export interface CreateCalendarChannelInput {id?: (Scalars['UUID'] | null),handle: Scalars['String'],visibility: CalendarChannelVisibility,syncStage: CalendarChannelSyncStage,connectedAccountId: Scalars['UUID'],isContactAutoCreationEnabled: Scalars['Boolean'],contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy,isSyncEnabled: Scalars['Boolean']}
|
||||
|
||||
export interface UpdateCalendarChannelInput {id: Scalars['UUID'],update: UpdateCalendarChannelInputUpdates}
|
||||
|
||||
export interface UpdateCalendarChannelInputUpdates {visibility?: (CalendarChannelVisibility | null),isContactAutoCreationEnabled?: (Scalars['Boolean'] | null),contactAutoCreationPolicy?: (CalendarChannelContactAutoCreationPolicy | null),isSyncEnabled?: (Scalars['Boolean'] | null)}
|
||||
|
||||
export interface CreateMessageChannelInput {id?: (Scalars['UUID'] | null),handle: Scalars['String'],visibility: MessageChannelVisibility,type: MessageChannelType,syncStage: MessageChannelSyncStage,connectedAccountId: Scalars['UUID'],isContactAutoCreationEnabled: Scalars['Boolean'],contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy,messageFolderImportPolicy: MessageFolderImportPolicy,excludeNonProfessionalEmails: Scalars['Boolean'],excludeGroupEmails: Scalars['Boolean'],pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction,isSyncEnabled: Scalars['Boolean']}
|
||||
|
||||
export interface UpdateMessageChannelInput {id: Scalars['UUID'],update: UpdateMessageChannelInputUpdates}
|
||||
|
||||
export interface UpdateMessageChannelInputUpdates {visibility?: (MessageChannelVisibility | null),isContactAutoCreationEnabled?: (Scalars['Boolean'] | null),contactAutoCreationPolicy?: (MessageChannelContactAutoCreationPolicy | null),messageFolderImportPolicy?: (MessageFolderImportPolicy | null),isSyncEnabled?: (Scalars['Boolean'] | null),excludeNonProfessionalEmails?: (Scalars['Boolean'] | null),excludeGroupEmails?: (Scalars['Boolean'] | null)}
|
||||
|
||||
export interface CreateConnectedAccountInput {id?: (Scalars['UUID'] | null),handle: Scalars['String'],provider: Scalars['String'],accessToken?: (Scalars['String'] | null),refreshToken?: (Scalars['String'] | null),scopes?: (Scalars['String'][] | null),userWorkspaceId: Scalars['UUID']}
|
||||
|
||||
export interface UpdateConnectedAccountInput {id: Scalars['UUID'],update: UpdateConnectedAccountInputUpdates}
|
||||
|
||||
export interface UpdateConnectedAccountInputUpdates {accessToken?: (Scalars['String'] | null),refreshToken?: (Scalars['String'] | null),handleAliases?: (Scalars['String'][] | null),scopes?: (Scalars['String'][] | null)}
|
||||
|
||||
export interface CreateWebhookInput {id?: (Scalars['UUID'] | null),targetUrl: Scalars['String'],operations: Scalars['String'][],description?: (Scalars['String'] | null),secret?: (Scalars['String'] | null)}
|
||||
|
||||
export interface UpdateWebhookInput {
|
||||
@@ -7647,6 +7895,38 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const CalendarChannel_possibleTypes: string[] = ['CalendarChannel']
|
||||
export const isCalendarChannel = (obj?: { __typename?: any } | null): obj is CalendarChannel => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isCalendarChannel"')
|
||||
return CalendarChannel_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ConnectedAccountDTO_possibleTypes: string[] = ['ConnectedAccountDTO']
|
||||
export const isConnectedAccountDTO = (obj?: { __typename?: any } | null): obj is ConnectedAccountDTO => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isConnectedAccountDTO"')
|
||||
return ConnectedAccountDTO_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const MessageChannel_possibleTypes: string[] = ['MessageChannel']
|
||||
export const isMessageChannel = (obj?: { __typename?: any } | null): obj is MessageChannel => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isMessageChannel"')
|
||||
return MessageChannel_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const MessageFolder_possibleTypes: string[] = ['MessageFolder']
|
||||
export const isMessageFolder = (obj?: { __typename?: any } | null): obj is MessageFolder => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isMessageFolder"')
|
||||
return MessageFolder_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const CollectionHash_possibleTypes: string[] = ['CollectionHash']
|
||||
export const isCollectionHash = (obj?: { __typename?: any } | null): obj is CollectionHash => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isCollectionHash"')
|
||||
@@ -8630,7 +8910,8 @@ export const enumFeatureFlagKey = {
|
||||
IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED: 'IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED' as const,
|
||||
IS_DRAFT_EMAIL_ENABLED: 'IS_DRAFT_EMAIL_ENABLED' as const,
|
||||
IS_RICH_TEXT_V1_MIGRATED: 'IS_RICH_TEXT_V1_MIGRATED' as const,
|
||||
IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED: 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' as const
|
||||
IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED: 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' as const,
|
||||
IS_CONNECTED_ACCOUNT_MIGRATED: 'IS_CONNECTED_ACCOUNT_MIGRATED' as const
|
||||
}
|
||||
|
||||
export const enumRelationType = {
|
||||
@@ -8733,6 +9014,89 @@ export const enumCommandMenuItemAvailabilityType = {
|
||||
FALLBACK: 'FALLBACK' as const
|
||||
}
|
||||
|
||||
export const enumCalendarChannelSyncStatus = {
|
||||
NOT_SYNCED: 'NOT_SYNCED' as const,
|
||||
ONGOING: 'ONGOING' as const,
|
||||
ACTIVE: 'ACTIVE' as const,
|
||||
FAILED_INSUFFICIENT_PERMISSIONS: 'FAILED_INSUFFICIENT_PERMISSIONS' as const,
|
||||
FAILED_UNKNOWN: 'FAILED_UNKNOWN' as const
|
||||
}
|
||||
|
||||
export const enumCalendarChannelSyncStage = {
|
||||
PENDING_CONFIGURATION: 'PENDING_CONFIGURATION' as const,
|
||||
CALENDAR_EVENT_LIST_FETCH_PENDING: 'CALENDAR_EVENT_LIST_FETCH_PENDING' as const,
|
||||
CALENDAR_EVENT_LIST_FETCH_SCHEDULED: 'CALENDAR_EVENT_LIST_FETCH_SCHEDULED' as const,
|
||||
CALENDAR_EVENT_LIST_FETCH_ONGOING: 'CALENDAR_EVENT_LIST_FETCH_ONGOING' as const,
|
||||
CALENDAR_EVENTS_IMPORT_PENDING: 'CALENDAR_EVENTS_IMPORT_PENDING' as const,
|
||||
CALENDAR_EVENTS_IMPORT_SCHEDULED: 'CALENDAR_EVENTS_IMPORT_SCHEDULED' as const,
|
||||
CALENDAR_EVENTS_IMPORT_ONGOING: 'CALENDAR_EVENTS_IMPORT_ONGOING' as const,
|
||||
FAILED: 'FAILED' as const
|
||||
}
|
||||
|
||||
export const enumCalendarChannelVisibility = {
|
||||
METADATA: 'METADATA' as const,
|
||||
SHARE_EVERYTHING: 'SHARE_EVERYTHING' as const
|
||||
}
|
||||
|
||||
export const enumCalendarChannelContactAutoCreationPolicy = {
|
||||
AS_PARTICIPANT_AND_ORGANIZER: 'AS_PARTICIPANT_AND_ORGANIZER' as const,
|
||||
AS_PARTICIPANT: 'AS_PARTICIPANT' as const,
|
||||
AS_ORGANIZER: 'AS_ORGANIZER' as const,
|
||||
NONE: 'NONE' as const
|
||||
}
|
||||
|
||||
export const enumMessageChannelVisibility = {
|
||||
METADATA: 'METADATA' as const,
|
||||
SUBJECT: 'SUBJECT' as const,
|
||||
SHARE_EVERYTHING: 'SHARE_EVERYTHING' as const
|
||||
}
|
||||
|
||||
export const enumMessageChannelType = {
|
||||
EMAIL: 'EMAIL' as const,
|
||||
SMS: 'SMS' as const
|
||||
}
|
||||
|
||||
export const enumMessageChannelContactAutoCreationPolicy = {
|
||||
SENT_AND_RECEIVED: 'SENT_AND_RECEIVED' as const,
|
||||
SENT: 'SENT' as const,
|
||||
NONE: 'NONE' as const
|
||||
}
|
||||
|
||||
export const enumMessageFolderImportPolicy = {
|
||||
ALL_FOLDERS: 'ALL_FOLDERS' as const,
|
||||
SELECTED_FOLDERS: 'SELECTED_FOLDERS' as const
|
||||
}
|
||||
|
||||
export const enumMessageChannelPendingGroupEmailsAction = {
|
||||
GROUP_EMAILS_DELETION: 'GROUP_EMAILS_DELETION' as const,
|
||||
GROUP_EMAILS_IMPORT: 'GROUP_EMAILS_IMPORT' as const,
|
||||
NONE: 'NONE' as const
|
||||
}
|
||||
|
||||
export const enumMessageChannelSyncStatus = {
|
||||
NOT_SYNCED: 'NOT_SYNCED' as const,
|
||||
ONGOING: 'ONGOING' as const,
|
||||
ACTIVE: 'ACTIVE' as const,
|
||||
FAILED_INSUFFICIENT_PERMISSIONS: 'FAILED_INSUFFICIENT_PERMISSIONS' as const,
|
||||
FAILED_UNKNOWN: 'FAILED_UNKNOWN' as const
|
||||
}
|
||||
|
||||
export const enumMessageChannelSyncStage = {
|
||||
PENDING_CONFIGURATION: 'PENDING_CONFIGURATION' as const,
|
||||
MESSAGE_LIST_FETCH_PENDING: 'MESSAGE_LIST_FETCH_PENDING' as const,
|
||||
MESSAGE_LIST_FETCH_SCHEDULED: 'MESSAGE_LIST_FETCH_SCHEDULED' as const,
|
||||
MESSAGE_LIST_FETCH_ONGOING: 'MESSAGE_LIST_FETCH_ONGOING' as const,
|
||||
MESSAGES_IMPORT_PENDING: 'MESSAGES_IMPORT_PENDING' as const,
|
||||
MESSAGES_IMPORT_SCHEDULED: 'MESSAGES_IMPORT_SCHEDULED' as const,
|
||||
MESSAGES_IMPORT_ONGOING: 'MESSAGES_IMPORT_ONGOING' as const,
|
||||
FAILED: 'FAILED' as const
|
||||
}
|
||||
|
||||
export const enumMessageFolderPendingSyncAction = {
|
||||
FOLDER_DELETION: 'FOLDER_DELETION' as const,
|
||||
NONE: 'NONE' as const
|
||||
}
|
||||
|
||||
export const enumAllMetadataName = {
|
||||
fieldMetadata: 'fieldMetadata' as const,
|
||||
objectMetadata: 'objectMetadata' as const,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+382
@@ -0,0 +1,382 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { Command } from 'nest-commander';
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
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 { WorkspaceEntity } from 'src/engine/core-modules/workspace/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 { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
|
||||
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';
|
||||
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import { type MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
|
||||
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-20:migrate-messaging-infrastructure-to-metadata',
|
||||
description:
|
||||
'Backfill connectedAccount, messageChannel, calendarChannel, and messageFolder to core metadata schema',
|
||||
})
|
||||
export class MigrateMessagingInfrastructureToMetadataCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
@InjectRepository(ConnectedAccountEntity)
|
||||
private readonly connectedAccountRepository: Repository<ConnectedAccountEntity>,
|
||||
@InjectRepository(MessageChannelEntity)
|
||||
private readonly messageChannelRepository: Repository<MessageChannelEntity>,
|
||||
@InjectRepository(CalendarChannelEntity)
|
||||
private readonly calendarChannelRepository: Repository<CalendarChannelEntity>,
|
||||
@InjectRepository(MessageFolderEntity)
|
||||
private readonly messageFolderRepository: Repository<MessageFolderEntity>,
|
||||
@InjectRepository(UserWorkspaceEntity)
|
||||
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const isDryRun = options.dryRun ?? false;
|
||||
|
||||
const isAlreadyMigrated = await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (isAlreadyMigrated) {
|
||||
this.logger.log(
|
||||
`IS_CONNECTED_ACCOUNT_MIGRATED already enabled for workspace ${workspaceId}, skipping`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const connectedAccountWorkspaceRepository =
|
||||
await this.twentyORMGlobalManager.getRepository<ConnectedAccountWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'connectedAccount',
|
||||
);
|
||||
|
||||
const messageChannelWorkspaceRepository =
|
||||
await this.twentyORMGlobalManager.getRepository<MessageChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'messageChannel',
|
||||
);
|
||||
|
||||
const calendarChannelWorkspaceRepository =
|
||||
await this.twentyORMGlobalManager.getRepository<CalendarChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'calendarChannel',
|
||||
);
|
||||
|
||||
const messageFolderWorkspaceRepository =
|
||||
await this.twentyORMGlobalManager.getRepository<MessageFolderWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'messageFolder',
|
||||
);
|
||||
|
||||
const connectedAccounts = await connectedAccountWorkspaceRepository.find();
|
||||
const messageChannels = await messageChannelWorkspaceRepository.find();
|
||||
const calendarChannels = await calendarChannelWorkspaceRepository.find();
|
||||
const messageFolders = await messageFolderWorkspaceRepository.find();
|
||||
|
||||
const workspaceMemberIdToUserWorkspaceIdMap =
|
||||
await this.buildWorkspaceMemberIdToUserWorkspaceIdMap(workspaceId);
|
||||
|
||||
const connectedAccountsWithMissingHandle = connectedAccounts.filter(
|
||||
(account) => !account.handle,
|
||||
);
|
||||
const connectedAccountsWithUnresolvedOwner = connectedAccounts.filter(
|
||||
(account) =>
|
||||
!workspaceMemberIdToUserWorkspaceIdMap.has(account.accountOwnerId),
|
||||
);
|
||||
const messageChannelsWithMissingHandle = messageChannels.filter(
|
||||
(channel) => !channel.handle,
|
||||
);
|
||||
const calendarChannelsWithMissingHandle = calendarChannels.filter(
|
||||
(channel) => !channel.handle,
|
||||
);
|
||||
|
||||
if (isDryRun) {
|
||||
this.logger.log(
|
||||
`[DRY RUN] Workspace ${workspaceId}: ` +
|
||||
`${connectedAccounts.length} connected accounts, ` +
|
||||
`${messageChannels.length} message channels, ` +
|
||||
`${calendarChannels.length} calendar channels, ` +
|
||||
`${messageFolders.length} message folders`,
|
||||
);
|
||||
|
||||
if (connectedAccountsWithMissingHandle.length > 0) {
|
||||
this.logger.warn(
|
||||
`[DRY RUN] ${connectedAccountsWithMissingHandle.length} connected accounts have empty handle`,
|
||||
);
|
||||
}
|
||||
|
||||
if (connectedAccountsWithUnresolvedOwner.length > 0) {
|
||||
this.logger.warn(
|
||||
`[DRY RUN] ${connectedAccountsWithUnresolvedOwner.length} connected accounts have unresolvable accountOwnerId (no matching userWorkspace)`,
|
||||
);
|
||||
}
|
||||
|
||||
if (messageChannelsWithMissingHandle.length > 0) {
|
||||
this.logger.warn(
|
||||
`[DRY RUN] ${messageChannelsWithMissingHandle.length} message channels have empty handle`,
|
||||
);
|
||||
}
|
||||
|
||||
if (calendarChannelsWithMissingHandle.length > 0) {
|
||||
this.logger.warn(
|
||||
`[DRY RUN] ${calendarChannelsWithMissingHandle.length} calendar channels have empty handle`,
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let migratedConnectedAccountIds = new Set(
|
||||
connectedAccounts.map((account) => account.id),
|
||||
);
|
||||
let migratedMessageChannelIds = new Set(
|
||||
messageChannels.map((channel) => channel.id),
|
||||
);
|
||||
|
||||
if (connectedAccounts.length > 0) {
|
||||
const coreConnectedAccounts = connectedAccounts
|
||||
.filter((workspaceEntity) => {
|
||||
const userWorkspaceId = workspaceMemberIdToUserWorkspaceIdMap.get(
|
||||
workspaceEntity.accountOwnerId,
|
||||
);
|
||||
|
||||
if (!userWorkspaceId) {
|
||||
this.logger.warn(
|
||||
`Skipping connected account ${workspaceEntity.id}: no userWorkspace found for workspaceMember ${workspaceEntity.accountOwnerId}`,
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.map((workspaceEntity) => {
|
||||
const handleAliases = isNonEmptyString(workspaceEntity.handleAliases)
|
||||
? workspaceEntity.handleAliases
|
||||
.split(',')
|
||||
.map((alias) => alias.trim())
|
||||
: null;
|
||||
|
||||
return {
|
||||
id: workspaceEntity.id,
|
||||
handle: workspaceEntity.handle ?? '',
|
||||
provider: workspaceEntity.provider,
|
||||
accessToken: workspaceEntity.accessToken,
|
||||
refreshToken: workspaceEntity.refreshToken,
|
||||
lastCredentialsRefreshedAt:
|
||||
workspaceEntity.lastCredentialsRefreshedAt,
|
||||
authFailedAt: workspaceEntity.authFailedAt,
|
||||
handleAliases,
|
||||
scopes: workspaceEntity.scopes,
|
||||
connectionParameters:
|
||||
workspaceEntity.connectionParameters as Record<
|
||||
string,
|
||||
unknown
|
||||
> | null,
|
||||
userWorkspaceId: workspaceMemberIdToUserWorkspaceIdMap.get(
|
||||
workspaceEntity.accountOwnerId,
|
||||
)!,
|
||||
workspaceId,
|
||||
createdAt: workspaceEntity.createdAt,
|
||||
updatedAt: workspaceEntity.updatedAt,
|
||||
};
|
||||
});
|
||||
|
||||
if (coreConnectedAccounts.length > 0) {
|
||||
await this.connectedAccountRepository.save(
|
||||
coreConnectedAccounts as unknown as ConnectedAccountEntity[],
|
||||
);
|
||||
this.logger.log(
|
||||
`Migrated ${coreConnectedAccounts.length} connected accounts for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
migratedConnectedAccountIds = new Set(
|
||||
coreConnectedAccounts.map((account) => account.id),
|
||||
);
|
||||
}
|
||||
|
||||
if (messageChannels.length > 0) {
|
||||
const coreMessageChannels = messageChannels
|
||||
.filter((workspaceEntity) =>
|
||||
migratedConnectedAccountIds.has(workspaceEntity.connectedAccountId),
|
||||
)
|
||||
.map((workspaceEntity) => ({
|
||||
id: workspaceEntity.id,
|
||||
visibility: workspaceEntity.visibility,
|
||||
handle: workspaceEntity.handle ?? '',
|
||||
type: workspaceEntity.type,
|
||||
isContactAutoCreationEnabled:
|
||||
workspaceEntity.isContactAutoCreationEnabled,
|
||||
contactAutoCreationPolicy: workspaceEntity.contactAutoCreationPolicy,
|
||||
messageFolderImportPolicy: workspaceEntity.messageFolderImportPolicy,
|
||||
excludeNonProfessionalEmails:
|
||||
workspaceEntity.excludeNonProfessionalEmails,
|
||||
excludeGroupEmails: workspaceEntity.excludeGroupEmails,
|
||||
pendingGroupEmailsAction: workspaceEntity.pendingGroupEmailsAction,
|
||||
isSyncEnabled: workspaceEntity.isSyncEnabled,
|
||||
syncCursor: workspaceEntity.syncCursor,
|
||||
syncedAt: workspaceEntity.syncedAt
|
||||
? new Date(workspaceEntity.syncedAt)
|
||||
: null,
|
||||
syncStatus: workspaceEntity.syncStatus ?? 'NOT_SYNCED',
|
||||
syncStage: workspaceEntity.syncStage,
|
||||
syncStageStartedAt: workspaceEntity.syncStageStartedAt
|
||||
? new Date(workspaceEntity.syncStageStartedAt)
|
||||
: null,
|
||||
throttleFailureCount: workspaceEntity.throttleFailureCount,
|
||||
throttleRetryAfter: workspaceEntity.throttleRetryAfter
|
||||
? new Date(workspaceEntity.throttleRetryAfter)
|
||||
: null,
|
||||
connectedAccountId: workspaceEntity.connectedAccountId,
|
||||
workspaceId,
|
||||
createdAt: workspaceEntity.createdAt,
|
||||
updatedAt: workspaceEntity.updatedAt,
|
||||
}));
|
||||
|
||||
if (coreMessageChannels.length > 0) {
|
||||
await this.messageChannelRepository.save(
|
||||
coreMessageChannels as unknown as MessageChannelEntity[],
|
||||
);
|
||||
this.logger.log(
|
||||
`Migrated ${coreMessageChannels.length} message channels for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
migratedMessageChannelIds = new Set(
|
||||
coreMessageChannels.map((channel) => channel.id),
|
||||
);
|
||||
}
|
||||
|
||||
if (calendarChannels.length > 0) {
|
||||
const coreCalendarChannels = calendarChannels
|
||||
.filter((workspaceEntity) =>
|
||||
migratedConnectedAccountIds.has(workspaceEntity.connectedAccountId),
|
||||
)
|
||||
.map((workspaceEntity) => ({
|
||||
id: workspaceEntity.id,
|
||||
handle: workspaceEntity.handle ?? '',
|
||||
syncStatus: workspaceEntity.syncStatus ?? 'NOT_SYNCED',
|
||||
syncStage: workspaceEntity.syncStage,
|
||||
visibility: workspaceEntity.visibility,
|
||||
isContactAutoCreationEnabled:
|
||||
workspaceEntity.isContactAutoCreationEnabled,
|
||||
contactAutoCreationPolicy: workspaceEntity.contactAutoCreationPolicy,
|
||||
isSyncEnabled: workspaceEntity.isSyncEnabled,
|
||||
syncCursor: workspaceEntity.syncCursor,
|
||||
syncedAt: workspaceEntity.syncedAt
|
||||
? new Date(workspaceEntity.syncedAt)
|
||||
: null,
|
||||
syncStageStartedAt: workspaceEntity.syncStageStartedAt
|
||||
? new Date(workspaceEntity.syncStageStartedAt)
|
||||
: null,
|
||||
throttleFailureCount: workspaceEntity.throttleFailureCount,
|
||||
connectedAccountId: workspaceEntity.connectedAccountId,
|
||||
workspaceId,
|
||||
createdAt: workspaceEntity.createdAt,
|
||||
updatedAt: workspaceEntity.updatedAt,
|
||||
}));
|
||||
|
||||
await this.calendarChannelRepository.save(
|
||||
coreCalendarChannels as unknown as CalendarChannelEntity[],
|
||||
);
|
||||
this.logger.log(
|
||||
`Migrated ${coreCalendarChannels.length} calendar channels for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (messageFolders.length > 0) {
|
||||
const coreMessageFolders = messageFolders
|
||||
.filter((workspaceEntity) =>
|
||||
migratedMessageChannelIds.has(workspaceEntity.messageChannelId),
|
||||
)
|
||||
.map((workspaceEntity) => ({
|
||||
id: workspaceEntity.id,
|
||||
name: workspaceEntity.name,
|
||||
syncCursor: workspaceEntity.syncCursor,
|
||||
isSentFolder: workspaceEntity.isSentFolder,
|
||||
isSynced: workspaceEntity.isSynced,
|
||||
parentFolderId: isNonEmptyString(workspaceEntity.parentFolderId)
|
||||
? workspaceEntity.parentFolderId
|
||||
: null,
|
||||
externalId: workspaceEntity.externalId,
|
||||
pendingSyncAction: workspaceEntity.pendingSyncAction,
|
||||
messageChannelId: workspaceEntity.messageChannelId,
|
||||
workspaceId,
|
||||
createdAt: workspaceEntity.createdAt,
|
||||
updatedAt: workspaceEntity.updatedAt,
|
||||
}));
|
||||
|
||||
await this.messageFolderRepository.save(
|
||||
coreMessageFolders as unknown as MessageFolderEntity[],
|
||||
);
|
||||
this.logger.log(
|
||||
`Migrated ${coreMessageFolders.length} message folders for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
await this.featureFlagService.enableFeatureFlags(
|
||||
[FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED],
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Enabled IS_CONNECTED_ACCOUNT_MIGRATED for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
private async buildWorkspaceMemberIdToUserWorkspaceIdMap(
|
||||
workspaceId: string,
|
||||
): Promise<Map<string, string>> {
|
||||
const workspaceMemberRepository =
|
||||
await this.twentyORMGlobalManager.getRepository<WorkspaceMemberWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workspaceMember',
|
||||
);
|
||||
|
||||
const workspaceMembers = await workspaceMemberRepository.find();
|
||||
const userWorkspaces = await this.userWorkspaceRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'userId'],
|
||||
});
|
||||
|
||||
const userWorkspaceIdByUserId = new Map(
|
||||
userWorkspaces.map((userWorkspace) => [
|
||||
userWorkspace.userId,
|
||||
userWorkspace.id,
|
||||
]),
|
||||
);
|
||||
|
||||
return new Map(
|
||||
workspaceMembers
|
||||
.filter((member) => userWorkspaceIdByUserId.has(member.userId))
|
||||
.map((member) => [
|
||||
member.id,
|
||||
userWorkspaceIdByUserId.get(member.userId)!,
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
+16
-1
@@ -8,13 +8,19 @@ import { IdentifyObjectPermissionMetadataCommand } from 'src/database/commands/u
|
||||
import { IdentifyPermissionFlagMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-permission-flag-metadata.command';
|
||||
import { MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-object-permission-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
import { MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-permission-flag-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
import { MigrateMessagingInfrastructureToMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-messaging-infrastructure-to-metadata.command';
|
||||
import { MigrateRichTextToTextCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-rich-text-to-text.command';
|
||||
import { SeedCliApplicationRegistrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-seed-cli-application-registration.command';
|
||||
import { ApplicationRegistrationModule } from 'src/engine/core-modules/application/application-registration/application-registration.module';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
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 { WorkspaceEntity } from 'src/engine/core-modules/workspace/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 { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
|
||||
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
@@ -23,7 +29,14 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([WorkspaceEntity]),
|
||||
TypeOrmModule.forFeature([
|
||||
WorkspaceEntity,
|
||||
ConnectedAccountEntity,
|
||||
MessageChannelEntity,
|
||||
CalendarChannelEntity,
|
||||
MessageFolderEntity,
|
||||
UserWorkspaceEntity,
|
||||
]),
|
||||
DataSourceModule,
|
||||
WorkspaceCacheModule,
|
||||
WorkspaceCacheStorageModule,
|
||||
@@ -44,6 +57,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
|
||||
BackfillPageLayoutsCommand,
|
||||
SeedCliApplicationRegistrationCommand,
|
||||
MigrateRichTextToTextCommand,
|
||||
MigrateMessagingInfrastructureToMetadataCommand,
|
||||
],
|
||||
exports: [
|
||||
IdentifyPermissionFlagMetadataCommand,
|
||||
@@ -55,6 +69,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
|
||||
BackfillPageLayoutsCommand,
|
||||
SeedCliApplicationRegistrationCommand,
|
||||
MigrateRichTextToTextCommand,
|
||||
MigrateMessagingInfrastructureToMetadataCommand,
|
||||
],
|
||||
})
|
||||
export class V1_20_UpgradeVersionCommandModule {}
|
||||
|
||||
+3
@@ -40,6 +40,7 @@ import { IdentifyObjectPermissionMetadataCommand } from 'src/database/commands/u
|
||||
import { IdentifyPermissionFlagMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-permission-flag-metadata.command';
|
||||
import { MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-object-permission-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
import { MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-permission-flag-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
import { MigrateMessagingInfrastructureToMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-messaging-infrastructure-to-metadata.command';
|
||||
import { MigrateRichTextToTextCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-rich-text-to-text.command';
|
||||
import { SeedCliApplicationRegistrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-seed-cli-application-registration.command';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
@@ -101,6 +102,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
protected readonly backfillPageLayoutsCommand: BackfillPageLayoutsCommand,
|
||||
protected readonly seedCliApplicationRegistrationCommand: SeedCliApplicationRegistrationCommand,
|
||||
protected readonly migrateRichTextToTextCommand: MigrateRichTextToTextCommand,
|
||||
protected readonly migrateMessagingInfrastructureToMetadataCommand: MigrateMessagingInfrastructureToMetadataCommand,
|
||||
) {
|
||||
super(
|
||||
workspaceRepository,
|
||||
@@ -159,6 +161,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
this.backfillCommandMenuItemsCommand,
|
||||
this.backfillPageLayoutsCommand,
|
||||
this.seedCliApplicationRegistrationCommand,
|
||||
this.migrateMessagingInfrastructureToMetadataCommand,
|
||||
];
|
||||
|
||||
this.allCommands = {
|
||||
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddMessagingInfrastructureMetadataEntities1773945207801
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddMessagingInfrastructureMetadataEntities1773945207801';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "core"."connectedAccount" ("workspaceId" uuid NOT NULL, "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "handle" character varying NOT NULL, "provider" character varying NOT NULL, "accessToken" character varying, "refreshToken" character varying, "lastCredentialsRefreshedAt" TIMESTAMP WITH TIME ZONE, "authFailedAt" TIMESTAMP WITH TIME ZONE, "handleAliases" character varying array, "scopes" character varying array, "connectionParameters" jsonb, "lastSignedInAt" TIMESTAMP WITH TIME ZONE, "oidcTokenClaims" jsonb, "userWorkspaceId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_8e7a0a0bbc2e06ac2acf89b7f3a" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."messageChannel_visibility_enum" AS ENUM('METADATA', 'SUBJECT', 'SHARE_EVERYTHING')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."messageChannel_type_enum" AS ENUM('EMAIL', 'SMS')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."messageChannel_contactautocreationpolicy_enum" AS ENUM('SENT_AND_RECEIVED', 'SENT', 'NONE')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."messageChannel_messagefolderimportpolicy_enum" AS ENUM('ALL_FOLDERS', 'SELECTED_FOLDERS')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."messageChannel_pendinggroupemailsaction_enum" AS ENUM('GROUP_EMAILS_DELETION', 'GROUP_EMAILS_IMPORT', 'NONE')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."messageChannel_syncstatus_enum" AS ENUM('NOT_SYNCED', 'ONGOING', 'ACTIVE', 'FAILED_INSUFFICIENT_PERMISSIONS', 'FAILED_UNKNOWN')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."messageChannel_syncstage_enum" AS ENUM('PENDING_CONFIGURATION', 'MESSAGE_LIST_FETCH_PENDING', 'MESSAGE_LIST_FETCH_SCHEDULED', 'MESSAGE_LIST_FETCH_ONGOING', 'MESSAGES_IMPORT_PENDING', 'MESSAGES_IMPORT_SCHEDULED', 'MESSAGES_IMPORT_ONGOING', 'FAILED')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "core"."messageChannel" ("workspaceId" uuid NOT NULL, "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "visibility" "core"."messageChannel_visibility_enum" NOT NULL, "handle" character varying NOT NULL, "type" "core"."messageChannel_type_enum" NOT NULL, "isContactAutoCreationEnabled" boolean NOT NULL, "contactAutoCreationPolicy" "core"."messageChannel_contactautocreationpolicy_enum" NOT NULL, "messageFolderImportPolicy" "core"."messageChannel_messagefolderimportpolicy_enum" NOT NULL, "excludeNonProfessionalEmails" boolean NOT NULL, "excludeGroupEmails" boolean NOT NULL, "pendingGroupEmailsAction" "core"."messageChannel_pendinggroupemailsaction_enum" NOT NULL, "isSyncEnabled" boolean NOT NULL, "syncCursor" character varying, "syncedAt" TIMESTAMP WITH TIME ZONE, "syncStatus" "core"."messageChannel_syncstatus_enum" NOT NULL DEFAULT 'NOT_SYNCED', "syncStage" "core"."messageChannel_syncstage_enum" NOT NULL, "syncStageStartedAt" TIMESTAMP WITH TIME ZONE, "throttleFailureCount" integer NOT NULL DEFAULT '0', "throttleRetryAfter" TIMESTAMP WITH TIME ZONE, "connectedAccountId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_438b9412475f39712ed065f77af" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."messageFolder_pendingsyncaction_enum" AS ENUM('FOLDER_DELETION', 'NONE')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "core"."messageFolder" ("workspaceId" uuid NOT NULL, "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying, "syncCursor" character varying, "isSentFolder" boolean NOT NULL, "isSynced" boolean NOT NULL, "parentFolderId" uuid, "externalId" character varying, "pendingSyncAction" "core"."messageFolder_pendingsyncaction_enum" NOT NULL, "messageChannelId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_85cb5a339d9f7f1106dde9e4db8" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."calendarChannel_syncstatus_enum" AS ENUM('NOT_SYNCED', 'ONGOING', 'ACTIVE', 'FAILED_INSUFFICIENT_PERMISSIONS', 'FAILED_UNKNOWN')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."calendarChannel_syncstage_enum" AS ENUM('PENDING_CONFIGURATION', 'CALENDAR_EVENT_LIST_FETCH_PENDING', 'CALENDAR_EVENT_LIST_FETCH_SCHEDULED', 'CALENDAR_EVENT_LIST_FETCH_ONGOING', 'CALENDAR_EVENTS_IMPORT_PENDING', 'CALENDAR_EVENTS_IMPORT_SCHEDULED', 'CALENDAR_EVENTS_IMPORT_ONGOING', 'FAILED')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."calendarChannel_visibility_enum" AS ENUM('METADATA', 'SHARE_EVERYTHING')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."calendarChannel_contactautocreationpolicy_enum" AS ENUM('AS_PARTICIPANT_AND_ORGANIZER', 'AS_PARTICIPANT', 'AS_ORGANIZER', 'NONE')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "core"."calendarChannel" ("workspaceId" uuid NOT NULL, "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "handle" character varying NOT NULL, "syncStatus" "core"."calendarChannel_syncstatus_enum" NOT NULL DEFAULT 'NOT_SYNCED', "syncStage" "core"."calendarChannel_syncstage_enum" NOT NULL, "visibility" "core"."calendarChannel_visibility_enum" NOT NULL, "isContactAutoCreationEnabled" boolean NOT NULL, "contactAutoCreationPolicy" "core"."calendarChannel_contactautocreationpolicy_enum" NOT NULL, "isSyncEnabled" boolean NOT NULL, "syncCursor" character varying, "syncedAt" TIMESTAMP WITH TIME ZONE, "syncStageStartedAt" TIMESTAMP WITH TIME ZONE, "throttleFailureCount" integer NOT NULL DEFAULT '0', "connectedAccountId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_796d701c0c35518517d0f3e0e0b" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."connectedAccount" ADD CONSTRAINT "FK_1c7af038a011e99c27044793c6a" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."messageChannel" ADD CONSTRAINT "FK_22d9a21a23fdd99295dc0efc177" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."messageChannel" ADD CONSTRAINT "FK_2e966cbb240771c67630d52895c" FOREIGN KEY ("connectedAccountId") REFERENCES "core"."connectedAccount"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."messageFolder" ADD CONSTRAINT "FK_e7fb85af997d06d8f7cc7512801" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."messageFolder" ADD CONSTRAINT "FK_4237a2fe8a6583354f807c2f8fe" FOREIGN KEY ("messageChannelId") REFERENCES "core"."messageChannel"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."calendarChannel" ADD CONSTRAINT "FK_bb5ebadf91b73c8050fb0a092fa" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."calendarChannel" ADD CONSTRAINT "FK_c7bc368c97a18a072413d67cf45" FOREIGN KEY ("connectedAccountId") REFERENCES "core"."connectedAccount"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."calendarChannel" DROP CONSTRAINT "FK_c7bc368c97a18a072413d67cf45"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."calendarChannel" DROP CONSTRAINT "FK_bb5ebadf91b73c8050fb0a092fa"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."messageFolder" DROP CONSTRAINT "FK_4237a2fe8a6583354f807c2f8fe"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."messageFolder" DROP CONSTRAINT "FK_e7fb85af997d06d8f7cc7512801"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."messageChannel" DROP CONSTRAINT "FK_2e966cbb240771c67630d52895c"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."messageChannel" DROP CONSTRAINT "FK_22d9a21a23fdd99295dc0efc177"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."connectedAccount" DROP CONSTRAINT "FK_1c7af038a011e99c27044793c6a"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "core"."calendarChannel"`);
|
||||
await queryRunner.query(
|
||||
`DROP TYPE "core"."calendarChannel_contactautocreationpolicy_enum"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP TYPE "core"."calendarChannel_visibility_enum"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP TYPE "core"."calendarChannel_syncstage_enum"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP TYPE "core"."calendarChannel_syncstatus_enum"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "core"."messageFolder"`);
|
||||
await queryRunner.query(
|
||||
`DROP TYPE "core"."messageFolder_pendingsyncaction_enum"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "core"."messageChannel"`);
|
||||
await queryRunner.query(`DROP TYPE "core"."messageChannel_syncstage_enum"`);
|
||||
await queryRunner.query(
|
||||
`DROP TYPE "core"."messageChannel_syncstatus_enum"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP TYPE "core"."messageChannel_pendinggroupemailsaction_enum"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP TYPE "core"."messageChannel_messagefolderimportpolicy_enum"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP TYPE "core"."messageChannel_contactautocreationpolicy_enum"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TYPE "core"."messageChannel_type_enum"`);
|
||||
await queryRunner.query(
|
||||
`DROP TYPE "core"."messageChannel_visibility_enum"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "core"."connectedAccount"`);
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,9 @@ import { WorkspaceInvitationModule } from 'src/engine/core-modules/workspace-inv
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.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 { MessageChannelDataAccessModule } from 'src/engine/metadata-modules/message-channel/data-access/message-channel-data-access.module';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
|
||||
@@ -104,6 +107,9 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
|
||||
ConnectedAccountModule,
|
||||
MessagingCommonModule,
|
||||
MessagingFolderSyncManagerModule,
|
||||
CalendarChannelDataAccessModule,
|
||||
ConnectedAccountDataAccessModule,
|
||||
MessageChannelDataAccessModule,
|
||||
WorkspaceSSOModule,
|
||||
FeatureFlagModule,
|
||||
WorkspaceInvitationModule,
|
||||
|
||||
+7
-10
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
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';
|
||||
@@ -9,7 +10,6 @@ import {
|
||||
CalendarChannelSyncStage,
|
||||
CalendarChannelSyncStatus,
|
||||
CalendarChannelVisibility,
|
||||
type CalendarChannelWorkspaceEntity,
|
||||
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
|
||||
export type CreateCalendarChannelInput = {
|
||||
@@ -25,6 +25,7 @@ export type CreateCalendarChannelInput = {
|
||||
export class CreateCalendarChannelService {
|
||||
constructor(
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService,
|
||||
) {}
|
||||
|
||||
async createCalendarChannel(
|
||||
@@ -43,15 +44,12 @@ export class CreateCalendarChannelService {
|
||||
|
||||
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
async () => {
|
||||
const calendarChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'calendarChannel',
|
||||
);
|
||||
const newCalendarChannelId = v4();
|
||||
|
||||
const newCalendarChannel = await calendarChannelRepository.save(
|
||||
await this.calendarChannelDataAccessService.save(
|
||||
workspaceId,
|
||||
{
|
||||
id: v4(),
|
||||
id: newCalendarChannelId,
|
||||
connectedAccountId,
|
||||
handle,
|
||||
visibility:
|
||||
@@ -63,11 +61,10 @@ export class CreateCalendarChannelService {
|
||||
? CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING
|
||||
: CalendarChannelSyncStage.PENDING_CONFIGURATION,
|
||||
},
|
||||
{},
|
||||
manager,
|
||||
);
|
||||
|
||||
return newCalendarChannel.id;
|
||||
return newCalendarChannelId;
|
||||
},
|
||||
authContext,
|
||||
);
|
||||
|
||||
+4
-9
@@ -2,10 +2,10 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type ConnectedAccountProvider } from 'twenty-shared/types';
|
||||
|
||||
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 { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
|
||||
export type CreateConnectedAccountInput = {
|
||||
workspaceId: string;
|
||||
@@ -23,6 +23,7 @@ export type CreateConnectedAccountInput = {
|
||||
export class CreateConnectedAccountService {
|
||||
constructor(
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
|
||||
) {}
|
||||
|
||||
async createConnectedAccount(
|
||||
@@ -43,13 +44,8 @@ export class CreateConnectedAccountService {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const connectedAccountRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'connectedAccount',
|
||||
);
|
||||
|
||||
await connectedAccountRepository.save(
|
||||
await this.connectedAccountDataAccessService.save(
|
||||
workspaceId,
|
||||
{
|
||||
id: connectedAccountId,
|
||||
handle,
|
||||
@@ -59,7 +55,6 @@ export class CreateConnectedAccountService {
|
||||
accountOwnerId,
|
||||
scopes,
|
||||
},
|
||||
{},
|
||||
manager,
|
||||
);
|
||||
}, authContext);
|
||||
|
||||
+11
-12
@@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
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';
|
||||
@@ -29,6 +30,7 @@ export type CreateMessageChannelInput = {
|
||||
export class CreateMessageChannelService {
|
||||
constructor(
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
|
||||
private readonly syncMessageFoldersService: SyncMessageFoldersService,
|
||||
) {}
|
||||
|
||||
@@ -48,15 +50,10 @@ export class CreateMessageChannelService {
|
||||
|
||||
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
async () => {
|
||||
const messageChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<MessageChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'messageChannel',
|
||||
);
|
||||
|
||||
const newMessageChannelId = v4();
|
||||
|
||||
await messageChannelRepository.insert(
|
||||
await this.messageChannelDataAccessService.save(
|
||||
workspaceId,
|
||||
{
|
||||
id: newMessageChannelId,
|
||||
connectedAccountId,
|
||||
@@ -76,17 +73,19 @@ export class CreateMessageChannelService {
|
||||
manager,
|
||||
);
|
||||
|
||||
const createdMessageChannel = await messageChannelRepository.findOne({
|
||||
where: { id: newMessageChannelId },
|
||||
relations: ['connectedAccount', 'messageFolders'],
|
||||
});
|
||||
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,
|
||||
messageChannel:
|
||||
createdMessageChannel as unknown as MessageChannelWorkspaceEntity,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
|
||||
+21
-18
@@ -11,6 +11,9 @@ 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';
|
||||
@@ -36,22 +39,16 @@ describe('GoogleAPIsService', () => {
|
||||
let messagingChannelSyncStatusService: MessageChannelSyncStatusService;
|
||||
let createMessageChannelService: CreateMessageChannelService;
|
||||
|
||||
const mockConnectedAccountRepository = {
|
||||
const mockConnectedAccountDataAccessService = {
|
||||
findOne: jest.fn(),
|
||||
find: jest.fn(),
|
||||
update: jest.fn(),
|
||||
};
|
||||
|
||||
const mockCalendarChannelRepository = {
|
||||
findOne: jest.fn(),
|
||||
const mockMessageChannelDataAccessService = {
|
||||
find: jest.fn(),
|
||||
update: jest.fn(),
|
||||
};
|
||||
|
||||
const mockMessageChannelRepository = {
|
||||
findOne: jest.fn(),
|
||||
const mockCalendarChannelDataAccessService = {
|
||||
find: jest.fn(),
|
||||
update: jest.fn(),
|
||||
};
|
||||
|
||||
const mockWorkspaceMemberRepository = {
|
||||
@@ -84,12 +81,6 @@ describe('GoogleAPIsService', () => {
|
||||
getRepository: jest
|
||||
.fn()
|
||||
.mockImplementation((_workspaceId, entity) => {
|
||||
if (entity === 'connectedAccount')
|
||||
return mockConnectedAccountRepository;
|
||||
if (entity === 'calendarChannel')
|
||||
return mockCalendarChannelRepository;
|
||||
if (entity === 'messageChannel')
|
||||
return mockMessageChannelRepository;
|
||||
if (entity === 'workspaceMember')
|
||||
return mockWorkspaceMemberRepository;
|
||||
|
||||
@@ -188,6 +179,18 @@ describe('GoogleAPIsService', () => {
|
||||
isFeatureEnabled: jest.fn().mockResolvedValue(false),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ConnectedAccountDataAccessService,
|
||||
useValue: mockConnectedAccountDataAccessService,
|
||||
},
|
||||
{
|
||||
provide: MessageChannelDataAccessService,
|
||||
useValue: mockMessageChannelDataAccessService,
|
||||
},
|
||||
{
|
||||
provide: CalendarChannelDataAccessService,
|
||||
useValue: mockCalendarChannelDataAccessService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@@ -221,7 +224,7 @@ describe('GoogleAPIsService', () => {
|
||||
provider: ConnectedAccountProvider.GOOGLE,
|
||||
} as ConnectedAccountWorkspaceEntity;
|
||||
|
||||
mockConnectedAccountRepository.findOne.mockResolvedValue(
|
||||
mockConnectedAccountDataAccessService.findOne.mockResolvedValue(
|
||||
existingConnectedAccount,
|
||||
);
|
||||
|
||||
@@ -237,11 +240,11 @@ describe('GoogleAPIsService', () => {
|
||||
syncStage: CalendarChannelSyncStage.FAILED,
|
||||
};
|
||||
|
||||
mockCalendarChannelRepository.find.mockResolvedValue([
|
||||
mockCalendarChannelDataAccessService.find.mockResolvedValue([
|
||||
failedCalendarChannel,
|
||||
]);
|
||||
|
||||
mockMessageChannelRepository.find.mockResolvedValue([]);
|
||||
mockMessageChannelDataAccessService.find.mockResolvedValue([]);
|
||||
|
||||
await service.refreshGoogleRefreshToken({
|
||||
handle: 'test@example.com',
|
||||
|
||||
+21
-31
@@ -18,6 +18,9 @@ import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decora
|
||||
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 { 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';
|
||||
@@ -29,16 +32,14 @@ import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/se
|
||||
import {
|
||||
CalendarChannelSyncStage,
|
||||
type CalendarChannelVisibility,
|
||||
type CalendarChannelWorkspaceEntity,
|
||||
} 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 {
|
||||
MessageChannelSyncStage,
|
||||
MessageChannelSyncStatus,
|
||||
type MessageChannelVisibility,
|
||||
type MessageChannelWorkspaceEntity,
|
||||
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import {
|
||||
MessagingMessageListFetchJob,
|
||||
@@ -65,6 +66,9 @@ 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,
|
||||
) {}
|
||||
|
||||
async refreshGoogleRefreshToken(input: {
|
||||
@@ -128,31 +132,14 @@ export class GoogleAPIsService {
|
||||
|
||||
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
async () => {
|
||||
const connectedAccountRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'connectedAccount',
|
||||
);
|
||||
|
||||
const connectedAccount = await connectedAccountRepository.findOne({
|
||||
where: { handle, accountOwnerId: workspaceMemberId },
|
||||
});
|
||||
const connectedAccount =
|
||||
await this.connectedAccountDataAccessService.findOne(workspaceId, {
|
||||
where: { handle, accountOwnerId: workspaceMemberId },
|
||||
});
|
||||
|
||||
const existingAccountId = connectedAccount?.id;
|
||||
const newOrExistingConnectedAccountId = existingAccountId ?? v4();
|
||||
|
||||
const calendarChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'calendarChannel',
|
||||
);
|
||||
|
||||
const messageChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<MessageChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'messageChannel',
|
||||
);
|
||||
|
||||
const workspaceDataSource =
|
||||
await this.globalWorkspaceOrmManager.getGlobalWorkspaceDataSource();
|
||||
|
||||
@@ -200,7 +187,6 @@ export class GoogleAPIsService {
|
||||
accessToken: input.accessToken,
|
||||
refreshToken: input.refreshToken,
|
||||
scopes,
|
||||
connectedAccount,
|
||||
manager,
|
||||
},
|
||||
);
|
||||
@@ -229,9 +215,10 @@ export class GoogleAPIsService {
|
||||
);
|
||||
|
||||
if (isMessagingEnabled) {
|
||||
const messageChannels = await messageChannelRepository.find({
|
||||
where: { connectedAccountId: newOrExistingConnectedAccountId },
|
||||
});
|
||||
const messageChannels =
|
||||
await this.messageChannelDataAccessService.find(workspaceId, {
|
||||
connectedAccountId: newOrExistingConnectedAccountId,
|
||||
});
|
||||
|
||||
if (!isMessagingAvailable && messageChannels.length > 0) {
|
||||
await this.messagingChannelSyncStatusService.markAsFailed(
|
||||
@@ -261,9 +248,12 @@ export class GoogleAPIsService {
|
||||
}
|
||||
|
||||
if (isCalendarEnabled) {
|
||||
const calendarChannels = await calendarChannelRepository.find({
|
||||
where: { connectedAccountId: newOrExistingConnectedAccountId },
|
||||
});
|
||||
const calendarChannels =
|
||||
await this.calendarChannelDataAccessService.find(workspaceId, {
|
||||
where: {
|
||||
connectedAccountId: newOrExistingConnectedAccountId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!isCalendarAvailable && calendarChannels.length > 0) {
|
||||
await this.calendarChannelSyncStatusService.markAsFailedInsufficientPermissionsAndFlushCalendarEventsToImport(
|
||||
|
||||
+21
-18
@@ -8,6 +8,9 @@ import { CreateConnectedAccountService } from 'src/engine/core-modules/auth/serv
|
||||
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';
|
||||
@@ -33,22 +36,16 @@ describe('MicrosoftAPIsService', () => {
|
||||
let calendarChannelSyncStatusService: CalendarChannelSyncStatusService;
|
||||
let createMessageChannelService: CreateMessageChannelService;
|
||||
|
||||
const mockConnectedAccountRepository = {
|
||||
const mockConnectedAccountDataAccessService = {
|
||||
findOne: jest.fn(),
|
||||
find: jest.fn(),
|
||||
update: jest.fn(),
|
||||
};
|
||||
|
||||
const mockCalendarChannelRepository = {
|
||||
findOne: jest.fn(),
|
||||
const mockMessageChannelDataAccessService = {
|
||||
find: jest.fn(),
|
||||
update: jest.fn(),
|
||||
};
|
||||
|
||||
const mockMessageChannelRepository = {
|
||||
findOne: jest.fn(),
|
||||
const mockCalendarChannelDataAccessService = {
|
||||
find: jest.fn(),
|
||||
update: jest.fn(),
|
||||
};
|
||||
|
||||
const mockWorkspaceMemberRepository = {
|
||||
@@ -81,12 +78,6 @@ describe('MicrosoftAPIsService', () => {
|
||||
getRepository: jest
|
||||
.fn()
|
||||
.mockImplementation((_workspaceId, entity) => {
|
||||
if (entity === 'connectedAccount')
|
||||
return mockConnectedAccountRepository;
|
||||
if (entity === 'calendarChannel')
|
||||
return mockCalendarChannelRepository;
|
||||
if (entity === 'messageChannel')
|
||||
return mockMessageChannelRepository;
|
||||
if (entity === 'workspaceMember')
|
||||
return mockWorkspaceMemberRepository;
|
||||
|
||||
@@ -162,6 +153,18 @@ describe('MicrosoftAPIsService', () => {
|
||||
provide: getQueueToken(MessageQueue.calendarQueue),
|
||||
useValue: mockCalendarQueueService,
|
||||
},
|
||||
{
|
||||
provide: ConnectedAccountDataAccessService,
|
||||
useValue: mockConnectedAccountDataAccessService,
|
||||
},
|
||||
{
|
||||
provide: MessageChannelDataAccessService,
|
||||
useValue: mockMessageChannelDataAccessService,
|
||||
},
|
||||
{
|
||||
provide: CalendarChannelDataAccessService,
|
||||
useValue: mockCalendarChannelDataAccessService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@@ -195,7 +198,7 @@ describe('MicrosoftAPIsService', () => {
|
||||
provider: ConnectedAccountProvider.MICROSOFT,
|
||||
} as ConnectedAccountWorkspaceEntity;
|
||||
|
||||
mockConnectedAccountRepository.findOne.mockResolvedValue(
|
||||
mockConnectedAccountDataAccessService.findOne.mockResolvedValue(
|
||||
existingConnectedAccount,
|
||||
);
|
||||
|
||||
@@ -211,11 +214,11 @@ describe('MicrosoftAPIsService', () => {
|
||||
syncStage: CalendarChannelSyncStage.FAILED,
|
||||
};
|
||||
|
||||
mockCalendarChannelRepository.find.mockResolvedValue([
|
||||
mockCalendarChannelDataAccessService.find.mockResolvedValue([
|
||||
failedCalendarChannel,
|
||||
]);
|
||||
|
||||
mockMessageChannelRepository.find.mockResolvedValue([
|
||||
mockMessageChannelDataAccessService.find.mockResolvedValue([
|
||||
{
|
||||
id: 'message-channel-id',
|
||||
connectedAccountId: 'existing-account-id',
|
||||
|
||||
+20
-34
@@ -12,6 +12,9 @@ import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decora
|
||||
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 { 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';
|
||||
@@ -23,15 +26,13 @@ import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/se
|
||||
import {
|
||||
CalendarChannelSyncStage,
|
||||
type CalendarChannelVisibility,
|
||||
type CalendarChannelWorkspaceEntity,
|
||||
} 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 {
|
||||
MessageChannelSyncStage,
|
||||
type MessageChannelVisibility,
|
||||
type MessageChannelWorkspaceEntity,
|
||||
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import {
|
||||
MessagingMessageListFetchJob,
|
||||
@@ -55,6 +56,9 @@ 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,
|
||||
) {}
|
||||
|
||||
async refreshMicrosoftRefreshToken(input: {
|
||||
@@ -82,31 +86,14 @@ export class MicrosoftAPIsService {
|
||||
|
||||
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
async () => {
|
||||
const connectedAccountRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'connectedAccount',
|
||||
);
|
||||
|
||||
const connectedAccount = await connectedAccountRepository.findOne({
|
||||
where: { handle, accountOwnerId: workspaceMemberId },
|
||||
});
|
||||
const connectedAccount =
|
||||
await this.connectedAccountDataAccessService.findOne(workspaceId, {
|
||||
where: { handle, accountOwnerId: workspaceMemberId },
|
||||
});
|
||||
|
||||
const existingAccountId = connectedAccount?.id;
|
||||
const newOrExistingConnectedAccountId = existingAccountId ?? v4();
|
||||
|
||||
const calendarChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'calendarChannel',
|
||||
);
|
||||
|
||||
const messageChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<MessageChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'messageChannel',
|
||||
);
|
||||
|
||||
const workspaceDataSource =
|
||||
await this.globalWorkspaceOrmManager.getGlobalWorkspaceDataSource();
|
||||
|
||||
@@ -156,7 +143,6 @@ export class MicrosoftAPIsService {
|
||||
accessToken: input.accessToken,
|
||||
refreshToken: input.refreshToken,
|
||||
scopes,
|
||||
connectedAccount,
|
||||
manager,
|
||||
},
|
||||
);
|
||||
@@ -197,11 +183,10 @@ export class MicrosoftAPIsService {
|
||||
if (
|
||||
this.twentyConfigService.get('MESSAGING_PROVIDER_MICROSOFT_ENABLED')
|
||||
) {
|
||||
const messageChannels = await messageChannelRepository.find({
|
||||
where: {
|
||||
const messageChannels =
|
||||
await this.messageChannelDataAccessService.find(workspaceId, {
|
||||
connectedAccountId: newOrExistingConnectedAccountId,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
for (const messageChannel of messageChannels) {
|
||||
if (
|
||||
@@ -222,11 +207,12 @@ export class MicrosoftAPIsService {
|
||||
if (
|
||||
this.twentyConfigService.get('CALENDAR_PROVIDER_MICROSOFT_ENABLED')
|
||||
) {
|
||||
const calendarChannels = await calendarChannelRepository.find({
|
||||
where: {
|
||||
connectedAccountId: newOrExistingConnectedAccountId,
|
||||
},
|
||||
});
|
||||
const calendarChannels =
|
||||
await this.calendarChannelDataAccessService.find(workspaceId, {
|
||||
where: {
|
||||
connectedAccountId: newOrExistingConnectedAccountId,
|
||||
},
|
||||
});
|
||||
|
||||
for (const calendarChannel of calendarChannels) {
|
||||
if (
|
||||
|
||||
+4
-11
@@ -1,9 +1,9 @@
|
||||
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 { 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 ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
|
||||
export type UpdateConnectedAccountOnReconnectInput = {
|
||||
workspaceId: string;
|
||||
@@ -11,7 +11,6 @@ export type UpdateConnectedAccountOnReconnectInput = {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
scopes: string[];
|
||||
connectedAccount: ConnectedAccountWorkspaceEntity;
|
||||
manager: WorkspaceEntityManager;
|
||||
};
|
||||
|
||||
@@ -19,6 +18,7 @@ export type UpdateConnectedAccountOnReconnectInput = {
|
||||
export class UpdateConnectedAccountOnReconnectService {
|
||||
constructor(
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
|
||||
) {}
|
||||
|
||||
async updateConnectedAccountOnReconnect(
|
||||
@@ -30,19 +30,13 @@ export class UpdateConnectedAccountOnReconnectService {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
scopes,
|
||||
manager,
|
||||
} = input;
|
||||
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const connectedAccountRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'connectedAccount',
|
||||
);
|
||||
|
||||
await connectedAccountRepository.update(
|
||||
await this.connectedAccountDataAccessService.update(
|
||||
workspaceId,
|
||||
{
|
||||
id: connectedAccountId,
|
||||
},
|
||||
@@ -52,7 +46,6 @@ export class UpdateConnectedAccountOnReconnectService {
|
||||
scopes,
|
||||
authFailedAt: null,
|
||||
},
|
||||
manager,
|
||||
);
|
||||
}, authContext);
|
||||
}
|
||||
|
||||
+2
@@ -3,6 +3,7 @@ import { Module } from '@nestjs/common';
|
||||
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 { 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';
|
||||
@@ -21,6 +22,7 @@ import { ImapSmtpCaldavService } from './services/imap-smtp-caldav-connection.se
|
||||
IMAPAPIsModule,
|
||||
MessagingImportManagerModule,
|
||||
MessageQueueModule,
|
||||
ConnectedAccountDataAccessModule,
|
||||
TwentyORMModule,
|
||||
FeatureFlagModule,
|
||||
ImapSmtpCaldavValidatorModule,
|
||||
|
||||
+10
-13
@@ -6,6 +6,7 @@ import { createTransport } from 'nodemailer';
|
||||
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
||||
|
||||
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 {
|
||||
type AccountType,
|
||||
type ConnectionParameters,
|
||||
@@ -21,6 +22,7 @@ export class ImapSmtpCaldavService {
|
||||
|
||||
constructor(
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
|
||||
) {}
|
||||
|
||||
async testImapConnection(
|
||||
@@ -188,20 +190,15 @@ export class ImapSmtpCaldavService {
|
||||
|
||||
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
async () => {
|
||||
const connectedAccountRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'connectedAccount',
|
||||
);
|
||||
const connectedAccount =
|
||||
await this.connectedAccountDataAccessService.findOne(workspaceId, {
|
||||
where: {
|
||||
id: connectionId,
|
||||
provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
|
||||
},
|
||||
});
|
||||
|
||||
const connectedAccount = await connectedAccountRepository.findOne({
|
||||
where: {
|
||||
id: connectionId,
|
||||
provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
|
||||
},
|
||||
});
|
||||
|
||||
return connectedAccount;
|
||||
return connectedAccount as ConnectedAccountWorkspaceEntity | null;
|
||||
},
|
||||
authContext,
|
||||
);
|
||||
|
||||
@@ -15,6 +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 { 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';
|
||||
@@ -35,6 +36,7 @@ import { MessagingSendManagerModule } from 'src/modules/messaging/message-outbou
|
||||
ViewModule,
|
||||
NavigationMenuItemModule,
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
ConnectedAccountDataAccessModule,
|
||||
],
|
||||
providers: [
|
||||
HttpTool,
|
||||
|
||||
+15
-22
@@ -20,11 +20,12 @@ 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 { ToolExecutionContext } from 'src/engine/core-modules/tool/types/tool.type';
|
||||
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 { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
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()
|
||||
@@ -33,6 +34,7 @@ export class EmailComposerService {
|
||||
|
||||
constructor(
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
|
||||
private readonly messagingAccountAuthenticationService: MessagingAccountAuthenticationService,
|
||||
@InjectRepository(FileEntity)
|
||||
private readonly fileRepository: Repository<FileEntity>,
|
||||
@@ -54,20 +56,15 @@ export class EmailComposerService {
|
||||
|
||||
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
async () => {
|
||||
const connectedAccountRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'connectedAccount',
|
||||
);
|
||||
|
||||
const connectedAccount = await connectedAccountRepository.findOne({
|
||||
where: { id: connectedAccountId },
|
||||
relations: {
|
||||
messageChannels: {
|
||||
messageFolders: true,
|
||||
const connectedAccount =
|
||||
await this.connectedAccountDataAccessService.findOne(workspaceId, {
|
||||
where: { id: connectedAccountId },
|
||||
relations: {
|
||||
messageChannels: {
|
||||
messageFolders: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
if (!isDefined(connectedAccount)) {
|
||||
throw new EmailToolException(
|
||||
@@ -76,7 +73,7 @@ export class EmailComposerService {
|
||||
);
|
||||
}
|
||||
|
||||
return connectedAccount;
|
||||
return connectedAccount as unknown as ConnectedAccountWorkspaceEntity;
|
||||
},
|
||||
authContext,
|
||||
);
|
||||
@@ -89,12 +86,8 @@ export class EmailComposerService {
|
||||
|
||||
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
async () => {
|
||||
const connectedAccountRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'connectedAccount',
|
||||
);
|
||||
const allAccounts = await connectedAccountRepository.find();
|
||||
const allAccounts =
|
||||
await this.connectedAccountDataAccessService.find(workspaceId);
|
||||
|
||||
if (!allAccounts || allAccounts.length === 0) {
|
||||
throw new EmailToolException(
|
||||
@@ -291,7 +284,7 @@ export class EmailComposerService {
|
||||
...connectedAccount,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
};
|
||||
} as unknown as ConnectedAccountWorkspaceEntity;
|
||||
|
||||
const attachments = await this.getAttachments(files || [], workspaceId);
|
||||
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { CalendarChannelMetadataService } from 'src/engine/metadata-modules/calendar-channel/calendar-channel-metadata.service';
|
||||
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
|
||||
import { CalendarChannelGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/calendar-channel/interceptors/calendar-channel-graphql-api-exception.interceptor';
|
||||
import { CalendarChannelResolver } from 'src/engine/metadata-modules/calendar-channel/resolvers/calendar-channel.resolver';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([CalendarChannelEntity]),
|
||||
AuthModule,
|
||||
PermissionsModule,
|
||||
FeatureFlagModule,
|
||||
],
|
||||
providers: [
|
||||
CalendarChannelMetadataService,
|
||||
CalendarChannelResolver,
|
||||
CalendarChannelGraphqlApiExceptionInterceptor,
|
||||
],
|
||||
exports: [CalendarChannelMetadataService],
|
||||
})
|
||||
export class CalendarChannelMetadataModule {}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
CalendarChannelSyncStage,
|
||||
CalendarChannelVisibility,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { CalendarChannelDTO } from 'src/engine/metadata-modules/calendar-channel/dtos/calendar-channel.dto';
|
||||
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
|
||||
|
||||
@Injectable()
|
||||
export class CalendarChannelMetadataService {
|
||||
constructor(
|
||||
@InjectRepository(CalendarChannelEntity)
|
||||
private readonly repository: Repository<CalendarChannelEntity>,
|
||||
) {}
|
||||
|
||||
async findAll(workspaceId: string): Promise<CalendarChannelDTO[]> {
|
||||
return this.repository.find({ where: { workspaceId } });
|
||||
}
|
||||
|
||||
async findByConnectedAccountId(
|
||||
connectedAccountId: string,
|
||||
workspaceId: string,
|
||||
): Promise<CalendarChannelDTO[]> {
|
||||
return this.repository.find({
|
||||
where: { connectedAccountId, workspaceId },
|
||||
});
|
||||
}
|
||||
|
||||
async findById(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
): Promise<CalendarChannelDTO | null> {
|
||||
return this.repository.findOne({ where: { id, workspaceId } });
|
||||
}
|
||||
|
||||
async create(
|
||||
data: Partial<CalendarChannelEntity> & {
|
||||
workspaceId: string;
|
||||
handle: string;
|
||||
connectedAccountId: string;
|
||||
visibility: CalendarChannelVisibility;
|
||||
syncStage: CalendarChannelSyncStage;
|
||||
},
|
||||
): Promise<CalendarChannelDTO> {
|
||||
const entity = this.repository.create(data);
|
||||
|
||||
return this.repository.save(entity);
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
data: Partial<CalendarChannelEntity>,
|
||||
): Promise<CalendarChannelDTO> {
|
||||
await this.repository.update(
|
||||
{ id, workspaceId },
|
||||
data as Record<string, unknown>,
|
||||
);
|
||||
|
||||
return this.repository.findOneOrFail({ where: { id, workspaceId } });
|
||||
}
|
||||
|
||||
async delete(id: string, workspaceId: string): Promise<CalendarChannelDTO> {
|
||||
const entity = await this.repository.findOneOrFail({
|
||||
where: { id, workspaceId },
|
||||
});
|
||||
|
||||
await this.repository.delete({ id, workspaceId });
|
||||
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { type MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export enum CalendarChannelExceptionCode {
|
||||
CALENDAR_CHANNEL_NOT_FOUND = 'CALENDAR_CHANNEL_NOT_FOUND',
|
||||
INVALID_CALENDAR_CHANNEL_INPUT = 'INVALID_CALENDAR_CHANNEL_INPUT',
|
||||
}
|
||||
|
||||
const getCalendarChannelExceptionUserFriendlyMessage = (
|
||||
code: CalendarChannelExceptionCode,
|
||||
) => {
|
||||
switch (code) {
|
||||
case CalendarChannelExceptionCode.CALENDAR_CHANNEL_NOT_FOUND:
|
||||
return msg`Calendar channel not found.`;
|
||||
case CalendarChannelExceptionCode.INVALID_CALENDAR_CHANNEL_INPUT:
|
||||
return msg`Invalid calendar channel input.`;
|
||||
default:
|
||||
assertUnreachable(code);
|
||||
}
|
||||
};
|
||||
|
||||
export class CalendarChannelException extends CustomException<CalendarChannelExceptionCode> {
|
||||
constructor(
|
||||
message: string,
|
||||
code: CalendarChannelExceptionCode,
|
||||
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
|
||||
) {
|
||||
super(message, code, {
|
||||
userFriendlyMessage:
|
||||
userFriendlyMessage ??
|
||||
getCalendarChannelExceptionUserFriendlyMessage(code),
|
||||
});
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
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 {}
|
||||
+280
@@ -0,0 +1,280 @@
|
||||
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);
|
||||
|
||||
return this.coreRepository.findOne({
|
||||
...options,
|
||||
where: coreWhere,
|
||||
} as FindOneOptions<CalendarChannelEntity>) as unknown as Promise<CalendarChannelWorkspaceEntity | null>;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
import { Field, HideField, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
IsBoolean,
|
||||
IsDateString,
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
import {
|
||||
CalendarChannelContactAutoCreationPolicy,
|
||||
CalendarChannelSyncStage,
|
||||
CalendarChannelSyncStatus,
|
||||
CalendarChannelVisibility,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@ObjectType('CalendarChannel')
|
||||
export class CalendarChannelDTO {
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
@Field(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
handle: string;
|
||||
|
||||
@IsEnum(CalendarChannelSyncStatus)
|
||||
@IsNotEmpty()
|
||||
@Field(() => CalendarChannelSyncStatus)
|
||||
syncStatus: CalendarChannelSyncStatus;
|
||||
|
||||
@IsEnum(CalendarChannelSyncStage)
|
||||
@IsNotEmpty()
|
||||
@Field(() => CalendarChannelSyncStage)
|
||||
syncStage: CalendarChannelSyncStage;
|
||||
|
||||
@IsEnum(CalendarChannelVisibility)
|
||||
@IsNotEmpty()
|
||||
@Field(() => CalendarChannelVisibility)
|
||||
visibility: CalendarChannelVisibility;
|
||||
|
||||
@IsBoolean()
|
||||
@Field()
|
||||
isContactAutoCreationEnabled: boolean;
|
||||
|
||||
@IsEnum(CalendarChannelContactAutoCreationPolicy)
|
||||
@IsNotEmpty()
|
||||
@Field(() => CalendarChannelContactAutoCreationPolicy)
|
||||
contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy;
|
||||
|
||||
@IsBoolean()
|
||||
@Field()
|
||||
isSyncEnabled: boolean;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field(() => String, { nullable: true })
|
||||
syncCursor: string | null;
|
||||
|
||||
@IsDateString()
|
||||
@IsOptional()
|
||||
@Field(() => Date, { nullable: true })
|
||||
syncedAt: Date | null;
|
||||
|
||||
@IsDateString()
|
||||
@IsOptional()
|
||||
@Field(() => Date, { nullable: true })
|
||||
syncStageStartedAt: Date | null;
|
||||
|
||||
@IsInt()
|
||||
@Field()
|
||||
throttleFailureCount: number;
|
||||
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
@Field(() => UUIDScalarType)
|
||||
connectedAccountId: string;
|
||||
|
||||
@HideField()
|
||||
workspaceId: string;
|
||||
|
||||
@IsDateString()
|
||||
@Field()
|
||||
createdAt: Date;
|
||||
|
||||
@IsDateString()
|
||||
@Field()
|
||||
updatedAt: Date;
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
import {
|
||||
CalendarChannelContactAutoCreationPolicy,
|
||||
CalendarChannelSyncStage,
|
||||
CalendarChannelVisibility,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@InputType()
|
||||
export class CreateCalendarChannelInput {
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
id?: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
handle: string;
|
||||
|
||||
@IsEnum(CalendarChannelVisibility)
|
||||
@IsNotEmpty()
|
||||
@Field(() => CalendarChannelVisibility)
|
||||
visibility: CalendarChannelVisibility;
|
||||
|
||||
@IsEnum(CalendarChannelSyncStage)
|
||||
@IsNotEmpty()
|
||||
@Field(() => CalendarChannelSyncStage)
|
||||
syncStage: CalendarChannelSyncStage;
|
||||
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
@Field(() => UUIDScalarType)
|
||||
connectedAccountId: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
isContactAutoCreationEnabled: boolean;
|
||||
|
||||
@IsEnum(CalendarChannelContactAutoCreationPolicy)
|
||||
@IsNotEmpty()
|
||||
@Field(() => CalendarChannelContactAutoCreationPolicy)
|
||||
contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy;
|
||||
|
||||
@IsBoolean()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
isSyncEnabled: boolean;
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsUUID,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import {
|
||||
CalendarChannelContactAutoCreationPolicy,
|
||||
CalendarChannelVisibility,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@InputType()
|
||||
export class UpdateCalendarChannelInputUpdates {
|
||||
@IsOptional()
|
||||
@IsEnum(CalendarChannelVisibility)
|
||||
@Field(() => CalendarChannelVisibility, { nullable: true })
|
||||
visibility?: CalendarChannelVisibility;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Field({ nullable: true })
|
||||
isContactAutoCreationEnabled?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(CalendarChannelContactAutoCreationPolicy)
|
||||
@Field(() => CalendarChannelContactAutoCreationPolicy, { nullable: true })
|
||||
contactAutoCreationPolicy?: CalendarChannelContactAutoCreationPolicy;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Field({ nullable: true })
|
||||
isSyncEnabled?: boolean;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
export class UpdateCalendarChannelInput {
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
@Field(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@Type(() => UpdateCalendarChannelInputUpdates)
|
||||
@ValidateNested()
|
||||
@Field(() => UpdateCalendarChannelInputUpdates)
|
||||
update: UpdateCalendarChannelInputUpdates;
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
type Relation,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import {
|
||||
CalendarChannelContactAutoCreationPolicy,
|
||||
CalendarChannelSyncStage,
|
||||
CalendarChannelSyncStatus,
|
||||
CalendarChannelVisibility,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/workspace-related-entity';
|
||||
|
||||
@Entity({ name: 'calendarChannel', schema: 'core' })
|
||||
export class CalendarChannelEntity extends WorkspaceRelatedEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'varchar', nullable: false })
|
||||
handle: string;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: CalendarChannelSyncStatus,
|
||||
nullable: false,
|
||||
default: CalendarChannelSyncStatus.NOT_SYNCED,
|
||||
})
|
||||
syncStatus: CalendarChannelSyncStatus;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: CalendarChannelSyncStage,
|
||||
nullable: false,
|
||||
})
|
||||
syncStage: CalendarChannelSyncStage;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: CalendarChannelVisibility,
|
||||
nullable: false,
|
||||
})
|
||||
visibility: CalendarChannelVisibility;
|
||||
|
||||
@Column({ type: 'boolean', nullable: false })
|
||||
isContactAutoCreationEnabled: boolean;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: CalendarChannelContactAutoCreationPolicy,
|
||||
nullable: false,
|
||||
})
|
||||
contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy;
|
||||
|
||||
@Column({ type: 'boolean', nullable: false })
|
||||
isSyncEnabled: boolean;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
syncCursor: string | null;
|
||||
|
||||
@Column({ type: 'timestamptz', nullable: true })
|
||||
syncedAt: Date | null;
|
||||
|
||||
@Column({ type: 'timestamptz', nullable: true })
|
||||
syncStageStartedAt: Date | null;
|
||||
|
||||
@Column({ type: 'integer', nullable: false, default: 0 })
|
||||
throttleFailureCount: number;
|
||||
|
||||
@Column({ type: 'uuid', nullable: false })
|
||||
connectedAccountId: string;
|
||||
|
||||
@ManyToOne(
|
||||
() => ConnectedAccountEntity,
|
||||
(connectedAccount) => connectedAccount.calendarChannels,
|
||||
{ onDelete: 'CASCADE' },
|
||||
)
|
||||
@JoinColumn({ name: 'connectedAccountId' })
|
||||
connectedAccount: Relation<ConnectedAccountEntity>;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
type CallHandler,
|
||||
type ExecutionContext,
|
||||
Injectable,
|
||||
type NestInterceptor,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { type Observable, catchError } from 'rxjs';
|
||||
|
||||
import { calendarChannelGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/calendar-channel/utils/calendar-channel-graphql-api-exception-handler.util';
|
||||
|
||||
@Injectable()
|
||||
export class CalendarChannelGraphqlApiExceptionInterceptor
|
||||
implements NestInterceptor
|
||||
{
|
||||
intercept(
|
||||
_context: ExecutionContext,
|
||||
next: CallHandler,
|
||||
): Observable<unknown> {
|
||||
return next
|
||||
.handle()
|
||||
.pipe(catchError(calendarChannelGraphqlApiExceptionHandler));
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
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 { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import {
|
||||
FeatureFlagGuard,
|
||||
RequireFeatureFlag,
|
||||
} from 'src/engine/guards/feature-flag.guard';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { CalendarChannelMetadataService } from 'src/engine/metadata-modules/calendar-channel/calendar-channel-metadata.service';
|
||||
import { CalendarChannelDTO } from 'src/engine/metadata-modules/calendar-channel/dtos/calendar-channel.dto';
|
||||
import { CreateCalendarChannelInput } from 'src/engine/metadata-modules/calendar-channel/dtos/create-calendar-channel.input';
|
||||
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)
|
||||
@UseInterceptors(CalendarChannelGraphqlApiExceptionInterceptor)
|
||||
@MetadataResolver(() => CalendarChannelDTO)
|
||||
export class CalendarChannelResolver {
|
||||
constructor(
|
||||
private readonly calendarChannelMetadataService: CalendarChannelMetadataService,
|
||||
) {}
|
||||
|
||||
@Query(() => [CalendarChannelDTO])
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async calendarChannels(
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@Args('connectedAccountId', {
|
||||
type: () => UUIDScalarType,
|
||||
nullable: true,
|
||||
})
|
||||
connectedAccountId?: string,
|
||||
): Promise<CalendarChannelDTO[]> {
|
||||
if (connectedAccountId) {
|
||||
return this.calendarChannelMetadataService.findByConnectedAccountId(
|
||||
connectedAccountId,
|
||||
workspace.id,
|
||||
);
|
||||
}
|
||||
|
||||
return this.calendarChannelMetadataService.findAll(workspace.id);
|
||||
}
|
||||
|
||||
@Query(() => CalendarChannelDTO, { nullable: true })
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async calendarChannel(
|
||||
@Args('id', { type: () => UUIDScalarType }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<CalendarChannelDTO | null> {
|
||||
return this.calendarChannelMetadataService.findById(id, workspace.id);
|
||||
}
|
||||
|
||||
@Mutation(() => CalendarChannelDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async createCalendarChannel(
|
||||
@Args('input') input: CreateCalendarChannelInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<CalendarChannelDTO> {
|
||||
return this.calendarChannelMetadataService.create({
|
||||
...input,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => CalendarChannelDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async updateCalendarChannel(
|
||||
@Args('input') input: UpdateCalendarChannelInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<CalendarChannelDTO> {
|
||||
return this.calendarChannelMetadataService.update(
|
||||
input.id,
|
||||
workspace.id,
|
||||
input.update,
|
||||
);
|
||||
}
|
||||
|
||||
@Mutation(() => CalendarChannelDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async deleteCalendarChannel(
|
||||
@Args('id', { type: () => UUIDScalarType }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<CalendarChannelDTO> {
|
||||
return this.calendarChannelMetadataService.delete(id, workspace.id);
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
NotFoundError,
|
||||
UserInputError,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import {
|
||||
CalendarChannelException,
|
||||
CalendarChannelExceptionCode,
|
||||
} from 'src/engine/metadata-modules/calendar-channel/calendar-channel.exception';
|
||||
|
||||
export const calendarChannelGraphqlApiExceptionHandler = (error: Error) => {
|
||||
if (error instanceof CalendarChannelException) {
|
||||
switch (error.code) {
|
||||
case CalendarChannelExceptionCode.CALENDAR_CHANNEL_NOT_FOUND:
|
||||
throw new NotFoundError(error);
|
||||
case CalendarChannelExceptionCode.INVALID_CALENDAR_CHANNEL_INPUT:
|
||||
throw new UserInputError(error);
|
||||
default: {
|
||||
return assertUnreachable(error.code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
};
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
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 { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([ConnectedAccountEntity]),
|
||||
AuthModule,
|
||||
PermissionsModule,
|
||||
FeatureFlagModule,
|
||||
],
|
||||
providers: [
|
||||
ConnectedAccountMetadataService,
|
||||
ConnectedAccountResolver,
|
||||
ConnectedAccountGraphqlApiExceptionInterceptor,
|
||||
],
|
||||
exports: [ConnectedAccountMetadataService],
|
||||
})
|
||||
export class ConnectedAccountMetadataModule {}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
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';
|
||||
|
||||
@Injectable()
|
||||
export class ConnectedAccountMetadataService {
|
||||
constructor(
|
||||
@InjectRepository(ConnectedAccountEntity)
|
||||
private readonly repository: Repository<ConnectedAccountEntity>,
|
||||
) {}
|
||||
|
||||
async findAll(workspaceId: string): Promise<ConnectedAccountDTO[]> {
|
||||
return this.repository.find({ where: { workspaceId } });
|
||||
}
|
||||
|
||||
async findById(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
): Promise<ConnectedAccountDTO | null> {
|
||||
return this.repository.findOne({ where: { id, workspaceId } });
|
||||
}
|
||||
|
||||
async create(
|
||||
data: Partial<ConnectedAccountEntity> & {
|
||||
workspaceId: string;
|
||||
handle: string;
|
||||
provider: string;
|
||||
userWorkspaceId: string;
|
||||
},
|
||||
): Promise<ConnectedAccountDTO> {
|
||||
const entity = this.repository.create(data);
|
||||
|
||||
return this.repository.save(entity);
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
data: Partial<ConnectedAccountEntity>,
|
||||
): Promise<ConnectedAccountDTO> {
|
||||
await this.repository.update(
|
||||
{ id, workspaceId },
|
||||
data as Record<string, unknown>,
|
||||
);
|
||||
|
||||
return this.repository.findOneOrFail({ where: { id, workspaceId } });
|
||||
}
|
||||
|
||||
async delete(id: string, workspaceId: string): Promise<ConnectedAccountDTO> {
|
||||
const entity = await this.repository.findOneOrFail({
|
||||
where: { id, workspaceId },
|
||||
});
|
||||
|
||||
await this.repository.delete({ id, workspaceId });
|
||||
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { type MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export enum ConnectedAccountExceptionCode {
|
||||
CONNECTED_ACCOUNT_NOT_FOUND = 'CONNECTED_ACCOUNT_NOT_FOUND',
|
||||
INVALID_CONNECTED_ACCOUNT_INPUT = 'INVALID_CONNECTED_ACCOUNT_INPUT',
|
||||
}
|
||||
|
||||
const getConnectedAccountExceptionUserFriendlyMessage = (
|
||||
code: ConnectedAccountExceptionCode,
|
||||
) => {
|
||||
switch (code) {
|
||||
case ConnectedAccountExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND:
|
||||
return msg`Connected account not found.`;
|
||||
case ConnectedAccountExceptionCode.INVALID_CONNECTED_ACCOUNT_INPUT:
|
||||
return msg`Invalid connected account input.`;
|
||||
default:
|
||||
assertUnreachable(code);
|
||||
}
|
||||
};
|
||||
|
||||
export class ConnectedAccountException extends CustomException<ConnectedAccountExceptionCode> {
|
||||
constructor(
|
||||
message: string,
|
||||
code: ConnectedAccountExceptionCode,
|
||||
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
|
||||
) {
|
||||
super(message, code, {
|
||||
userFriendlyMessage:
|
||||
userFriendlyMessage ??
|
||||
getConnectedAccountExceptionUserFriendlyMessage(code),
|
||||
});
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
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 {}
|
||||
+346
@@ -0,0 +1,346 @@
|
||||
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) {
|
||||
coreData.handleAliases = isNonEmptyString(handleAliases)
|
||||
? handleAliases.split(',').map((alias: string) => alias.trim())
|
||||
: 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import { Field, HideField, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
IsArray,
|
||||
IsDateString,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
import GraphQLJSON from 'graphql-type-json';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@ObjectType('ConnectedAccountDTO')
|
||||
export class ConnectedAccountDTO {
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
@Field(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
handle: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
provider: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field(() => String, { nullable: true })
|
||||
accessToken: string | null;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field(() => String, { nullable: true })
|
||||
refreshToken: string | null;
|
||||
|
||||
@IsDateString()
|
||||
@IsOptional()
|
||||
@Field(() => Date, { nullable: true })
|
||||
lastCredentialsRefreshedAt: Date | null;
|
||||
|
||||
@IsDateString()
|
||||
@IsOptional()
|
||||
@Field(() => Date, { nullable: true })
|
||||
authFailedAt: Date | null;
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
@Field(() => [String], { nullable: true })
|
||||
handleAliases: string[] | null;
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
@Field(() => [String], { nullable: true })
|
||||
scopes: string[] | null;
|
||||
|
||||
@IsOptional()
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
connectionParameters: Record<string, unknown> | null;
|
||||
|
||||
@IsDateString()
|
||||
@IsOptional()
|
||||
@Field(() => Date, { nullable: true })
|
||||
lastSignedInAt: Date | null;
|
||||
|
||||
@IsOptional()
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
oidcTokenClaims: Record<string, unknown> | null;
|
||||
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
@Field(() => UUIDScalarType)
|
||||
userWorkspaceId: string;
|
||||
|
||||
@HideField()
|
||||
workspaceId: string;
|
||||
|
||||
@IsDateString()
|
||||
@Field()
|
||||
createdAt: Date;
|
||||
|
||||
@IsDateString()
|
||||
@Field()
|
||||
updatedAt: Date;
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
IsArray,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@InputType()
|
||||
export class CreateConnectedAccountInput {
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
id?: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
handle: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
provider: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
accessToken?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
refreshToken?: string;
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
@Field(() => [String], { nullable: true })
|
||||
scopes?: string[];
|
||||
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
@Field(() => UUIDScalarType)
|
||||
userWorkspaceId: string;
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsArray,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@InputType()
|
||||
export class UpdateConnectedAccountInputUpdates {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
accessToken?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
refreshToken?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@Field(() => [String], { nullable: true })
|
||||
handleAliases?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@Field(() => [String], { nullable: true })
|
||||
scopes?: string[];
|
||||
}
|
||||
|
||||
@InputType()
|
||||
export class UpdateConnectedAccountInput {
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
@Field(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@Type(() => UpdateConnectedAccountInputUpdates)
|
||||
@ValidateNested()
|
||||
@Field(() => UpdateConnectedAccountInputUpdates)
|
||||
update: UpdateConnectedAccountInputUpdates;
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
OneToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
type Relation,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
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';
|
||||
|
||||
@Entity({ name: 'connectedAccount', schema: 'core' })
|
||||
export class ConnectedAccountEntity extends WorkspaceRelatedEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'varchar', nullable: false })
|
||||
handle: string;
|
||||
|
||||
@Column({ type: 'varchar', nullable: false })
|
||||
provider: string;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
accessToken: string | null;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
refreshToken: string | null;
|
||||
|
||||
@Column({ type: 'timestamptz', nullable: true })
|
||||
lastCredentialsRefreshedAt: Date | null;
|
||||
|
||||
@Column({ type: 'timestamptz', nullable: true })
|
||||
authFailedAt: Date | null;
|
||||
|
||||
@Column({ type: 'varchar', array: true, nullable: true })
|
||||
handleAliases: string[] | null;
|
||||
|
||||
@Column({ type: 'varchar', array: true, nullable: true })
|
||||
scopes: string[] | null;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
connectionParameters: Record<string, unknown> | null;
|
||||
|
||||
@Column({ type: 'timestamptz', nullable: true })
|
||||
lastSignedInAt: Date | null;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
oidcTokenClaims: Record<string, unknown> | null;
|
||||
|
||||
@Column({ type: 'uuid', nullable: false })
|
||||
userWorkspaceId: string;
|
||||
|
||||
@OneToMany(
|
||||
'MessageChannelEntity',
|
||||
(messageChannel: MessageChannelEntity) => messageChannel.connectedAccount,
|
||||
)
|
||||
messageChannels: Relation<MessageChannelEntity[]>;
|
||||
|
||||
@OneToMany(
|
||||
'CalendarChannelEntity',
|
||||
(calendarChannel: CalendarChannelEntity) =>
|
||||
calendarChannel.connectedAccount,
|
||||
)
|
||||
calendarChannels: Relation<CalendarChannelEntity[]>;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
type CallHandler,
|
||||
type ExecutionContext,
|
||||
Injectable,
|
||||
type NestInterceptor,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { type Observable, catchError } from 'rxjs';
|
||||
|
||||
import { connectedAccountGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/connected-account/utils/connected-account-graphql-api-exception-handler.util';
|
||||
|
||||
@Injectable()
|
||||
export class ConnectedAccountGraphqlApiExceptionInterceptor
|
||||
implements NestInterceptor
|
||||
{
|
||||
intercept(
|
||||
_context: ExecutionContext,
|
||||
next: CallHandler,
|
||||
): Observable<unknown> {
|
||||
return next
|
||||
.handle()
|
||||
.pipe(catchError(connectedAccountGraphqlApiExceptionHandler));
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
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 { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import {
|
||||
FeatureFlagGuard,
|
||||
RequireFeatureFlag,
|
||||
} from 'src/engine/guards/feature-flag.guard';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { ConnectedAccountMetadataService } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.service';
|
||||
import { ConnectedAccountDTO } from 'src/engine/metadata-modules/connected-account/dtos/connected-account.dto';
|
||||
import { CreateConnectedAccountInput } from 'src/engine/metadata-modules/connected-account/dtos/create-connected-account.input';
|
||||
import { UpdateConnectedAccountInput } from 'src/engine/metadata-modules/connected-account/dtos/update-connected-account.input';
|
||||
import { ConnectedAccountGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/connected-account/interceptors/connected-account-graphql-api-exception.interceptor';
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard, FeatureFlagGuard)
|
||||
@UseInterceptors(ConnectedAccountGraphqlApiExceptionInterceptor)
|
||||
@MetadataResolver(() => ConnectedAccountDTO)
|
||||
export class ConnectedAccountResolver {
|
||||
constructor(
|
||||
private readonly connectedAccountMetadataService: ConnectedAccountMetadataService,
|
||||
) {}
|
||||
|
||||
@Query(() => [ConnectedAccountDTO])
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async connectedAccounts(
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<ConnectedAccountDTO[]> {
|
||||
return this.connectedAccountMetadataService.findAll(workspace.id);
|
||||
}
|
||||
|
||||
@Query(() => ConnectedAccountDTO, { nullable: true })
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async connectedAccount(
|
||||
@Args('id', { type: () => UUIDScalarType }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<ConnectedAccountDTO | null> {
|
||||
return this.connectedAccountMetadataService.findById(id, workspace.id);
|
||||
}
|
||||
|
||||
@Mutation(() => ConnectedAccountDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async createConnectedAccount(
|
||||
@Args('input') input: CreateConnectedAccountInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<ConnectedAccountDTO> {
|
||||
return this.connectedAccountMetadataService.create({
|
||||
...input,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => ConnectedAccountDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async updateConnectedAccount(
|
||||
@Args('input') input: UpdateConnectedAccountInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<ConnectedAccountDTO> {
|
||||
return this.connectedAccountMetadataService.update(
|
||||
input.id,
|
||||
workspace.id,
|
||||
input.update,
|
||||
);
|
||||
}
|
||||
|
||||
@Mutation(() => ConnectedAccountDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async deleteConnectedAccount(
|
||||
@Args('id', { type: () => UUIDScalarType }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<ConnectedAccountDTO> {
|
||||
return this.connectedAccountMetadataService.delete(id, workspace.id);
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
NotFoundError,
|
||||
UserInputError,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import {
|
||||
ConnectedAccountException,
|
||||
ConnectedAccountExceptionCode,
|
||||
} from 'src/engine/metadata-modules/connected-account/connected-account.exception';
|
||||
|
||||
export const connectedAccountGraphqlApiExceptionHandler = (error: Error) => {
|
||||
if (error instanceof ConnectedAccountException) {
|
||||
switch (error.code) {
|
||||
case ConnectedAccountExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND:
|
||||
throw new NotFoundError(error);
|
||||
case ConnectedAccountExceptionCode.INVALID_CONNECTED_ACCOUNT_INPUT:
|
||||
throw new UserInputError(error);
|
||||
default: {
|
||||
return assertUnreachable(error.code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
};
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
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';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([MessageChannelEntity]),
|
||||
FeatureFlagModule,
|
||||
ConnectedAccountDataAccessModule,
|
||||
],
|
||||
providers: [MessageChannelDataAccessService],
|
||||
exports: [MessageChannelDataAccessService],
|
||||
})
|
||||
export class MessageChannelDataAccessModule {}
|
||||
+280
@@ -0,0 +1,280 @@
|
||||
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 { 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 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);
|
||||
|
||||
return this.coreRepository.findOne({
|
||||
...options,
|
||||
where: coreWhere,
|
||||
} as FindOneOptions<MessageChannelEntity>) as unknown as Promise<MessageChannelWorkspaceEntity | null>;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
import {
|
||||
MessageChannelContactAutoCreationPolicy,
|
||||
MessageChannelPendingGroupEmailsAction,
|
||||
MessageChannelSyncStage,
|
||||
MessageChannelType,
|
||||
MessageChannelVisibility,
|
||||
MessageFolderImportPolicy,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@InputType()
|
||||
export class CreateMessageChannelInput {
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
id?: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
handle: string;
|
||||
|
||||
@IsEnum(MessageChannelVisibility)
|
||||
@IsNotEmpty()
|
||||
@Field(() => MessageChannelVisibility)
|
||||
visibility: MessageChannelVisibility;
|
||||
|
||||
@IsEnum(MessageChannelType)
|
||||
@IsNotEmpty()
|
||||
@Field(() => MessageChannelType)
|
||||
type: MessageChannelType;
|
||||
|
||||
@IsEnum(MessageChannelSyncStage)
|
||||
@IsNotEmpty()
|
||||
@Field(() => MessageChannelSyncStage)
|
||||
syncStage: MessageChannelSyncStage;
|
||||
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
@Field(() => UUIDScalarType)
|
||||
connectedAccountId: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
isContactAutoCreationEnabled: boolean;
|
||||
|
||||
@IsEnum(MessageChannelContactAutoCreationPolicy)
|
||||
@IsNotEmpty()
|
||||
@Field(() => MessageChannelContactAutoCreationPolicy)
|
||||
contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy;
|
||||
|
||||
@IsEnum(MessageFolderImportPolicy)
|
||||
@IsNotEmpty()
|
||||
@Field(() => MessageFolderImportPolicy)
|
||||
messageFolderImportPolicy: MessageFolderImportPolicy;
|
||||
|
||||
@IsBoolean()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
excludeNonProfessionalEmails: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
excludeGroupEmails: boolean;
|
||||
|
||||
@IsEnum(MessageChannelPendingGroupEmailsAction)
|
||||
@IsNotEmpty()
|
||||
@Field(() => MessageChannelPendingGroupEmailsAction)
|
||||
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction;
|
||||
|
||||
@IsBoolean()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
isSyncEnabled: boolean;
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
import { Field, HideField, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
IsBoolean,
|
||||
IsDateString,
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
import {
|
||||
MessageChannelContactAutoCreationPolicy,
|
||||
MessageChannelPendingGroupEmailsAction,
|
||||
MessageChannelSyncStage,
|
||||
MessageChannelSyncStatus,
|
||||
MessageChannelType,
|
||||
MessageChannelVisibility,
|
||||
MessageFolderImportPolicy,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@ObjectType('MessageChannel')
|
||||
export class MessageChannelDTO {
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
@Field(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@IsEnum(MessageChannelVisibility)
|
||||
@IsNotEmpty()
|
||||
@Field(() => MessageChannelVisibility)
|
||||
visibility: MessageChannelVisibility;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
handle: string;
|
||||
|
||||
@IsEnum(MessageChannelType)
|
||||
@IsNotEmpty()
|
||||
@Field(() => MessageChannelType)
|
||||
type: MessageChannelType;
|
||||
|
||||
@IsBoolean()
|
||||
@Field()
|
||||
isContactAutoCreationEnabled: boolean;
|
||||
|
||||
@IsEnum(MessageChannelContactAutoCreationPolicy)
|
||||
@IsNotEmpty()
|
||||
@Field(() => MessageChannelContactAutoCreationPolicy)
|
||||
contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy;
|
||||
|
||||
@IsEnum(MessageFolderImportPolicy)
|
||||
@IsNotEmpty()
|
||||
@Field(() => MessageFolderImportPolicy)
|
||||
messageFolderImportPolicy: MessageFolderImportPolicy;
|
||||
|
||||
@IsBoolean()
|
||||
@Field()
|
||||
excludeNonProfessionalEmails: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@Field()
|
||||
excludeGroupEmails: boolean;
|
||||
|
||||
@IsEnum(MessageChannelPendingGroupEmailsAction)
|
||||
@IsNotEmpty()
|
||||
@Field(() => MessageChannelPendingGroupEmailsAction)
|
||||
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction;
|
||||
|
||||
@IsBoolean()
|
||||
@Field()
|
||||
isSyncEnabled: boolean;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field(() => String, { nullable: true })
|
||||
syncCursor: string | null;
|
||||
|
||||
@IsDateString()
|
||||
@IsOptional()
|
||||
@Field(() => Date, { nullable: true })
|
||||
syncedAt: Date | null;
|
||||
|
||||
@IsEnum(MessageChannelSyncStatus)
|
||||
@IsNotEmpty()
|
||||
@Field(() => MessageChannelSyncStatus)
|
||||
syncStatus: MessageChannelSyncStatus;
|
||||
|
||||
@IsEnum(MessageChannelSyncStage)
|
||||
@IsNotEmpty()
|
||||
@Field(() => MessageChannelSyncStage)
|
||||
syncStage: MessageChannelSyncStage;
|
||||
|
||||
@IsDateString()
|
||||
@IsOptional()
|
||||
@Field(() => Date, { nullable: true })
|
||||
syncStageStartedAt: Date | null;
|
||||
|
||||
@IsInt()
|
||||
@Field()
|
||||
throttleFailureCount: number;
|
||||
|
||||
@IsDateString()
|
||||
@IsOptional()
|
||||
@Field(() => Date, { nullable: true })
|
||||
throttleRetryAfter: Date | null;
|
||||
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
@Field(() => UUIDScalarType)
|
||||
connectedAccountId: string;
|
||||
|
||||
@HideField()
|
||||
workspaceId: string;
|
||||
|
||||
@IsDateString()
|
||||
@Field()
|
||||
createdAt: Date;
|
||||
|
||||
@IsDateString()
|
||||
@Field()
|
||||
updatedAt: Date;
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsUUID,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import {
|
||||
MessageChannelContactAutoCreationPolicy,
|
||||
MessageChannelVisibility,
|
||||
MessageFolderImportPolicy,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@InputType()
|
||||
export class UpdateMessageChannelInputUpdates {
|
||||
@IsOptional()
|
||||
@IsEnum(MessageChannelVisibility)
|
||||
@Field(() => MessageChannelVisibility, { nullable: true })
|
||||
visibility?: MessageChannelVisibility;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Field({ nullable: true })
|
||||
isContactAutoCreationEnabled?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(MessageChannelContactAutoCreationPolicy)
|
||||
@Field(() => MessageChannelContactAutoCreationPolicy, { nullable: true })
|
||||
contactAutoCreationPolicy?: MessageChannelContactAutoCreationPolicy;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(MessageFolderImportPolicy)
|
||||
@Field(() => MessageFolderImportPolicy, { nullable: true })
|
||||
messageFolderImportPolicy?: MessageFolderImportPolicy;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Field({ nullable: true })
|
||||
isSyncEnabled?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Field({ nullable: true })
|
||||
excludeNonProfessionalEmails?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Field({ nullable: true })
|
||||
excludeGroupEmails?: boolean;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
export class UpdateMessageChannelInput {
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
@Field(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@Type(() => UpdateMessageChannelInputUpdates)
|
||||
@ValidateNested()
|
||||
@Field(() => UpdateMessageChannelInputUpdates)
|
||||
update: UpdateMessageChannelInputUpdates;
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
type Relation,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import {
|
||||
MessageChannelContactAutoCreationPolicy,
|
||||
MessageChannelPendingGroupEmailsAction,
|
||||
MessageChannelSyncStage,
|
||||
MessageChannelSyncStatus,
|
||||
MessageChannelType,
|
||||
MessageChannelVisibility,
|
||||
MessageFolderImportPolicy,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
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';
|
||||
|
||||
@Entity({ name: 'messageChannel', schema: 'core' })
|
||||
export class MessageChannelEntity extends WorkspaceRelatedEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: MessageChannelVisibility,
|
||||
nullable: false,
|
||||
})
|
||||
visibility: MessageChannelVisibility;
|
||||
|
||||
@Column({ type: 'varchar', nullable: false })
|
||||
handle: string;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: MessageChannelType,
|
||||
nullable: false,
|
||||
})
|
||||
type: MessageChannelType;
|
||||
|
||||
@Column({ type: 'boolean', nullable: false })
|
||||
isContactAutoCreationEnabled: boolean;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: MessageChannelContactAutoCreationPolicy,
|
||||
nullable: false,
|
||||
})
|
||||
contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: MessageFolderImportPolicy,
|
||||
nullable: false,
|
||||
})
|
||||
messageFolderImportPolicy: MessageFolderImportPolicy;
|
||||
|
||||
@Column({ type: 'boolean', nullable: false })
|
||||
excludeNonProfessionalEmails: boolean;
|
||||
|
||||
@Column({ type: 'boolean', nullable: false })
|
||||
excludeGroupEmails: boolean;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: MessageChannelPendingGroupEmailsAction,
|
||||
nullable: false,
|
||||
})
|
||||
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction;
|
||||
|
||||
@Column({ type: 'boolean', nullable: false })
|
||||
isSyncEnabled: boolean;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
syncCursor: string | null;
|
||||
|
||||
@Column({ type: 'timestamptz', nullable: true })
|
||||
syncedAt: Date | null;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: MessageChannelSyncStatus,
|
||||
nullable: false,
|
||||
default: MessageChannelSyncStatus.NOT_SYNCED,
|
||||
})
|
||||
syncStatus: MessageChannelSyncStatus;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: MessageChannelSyncStage,
|
||||
nullable: false,
|
||||
})
|
||||
syncStage: MessageChannelSyncStage;
|
||||
|
||||
@Column({ type: 'timestamptz', nullable: true })
|
||||
syncStageStartedAt: Date | null;
|
||||
|
||||
@Column({ type: 'integer', nullable: false, default: 0 })
|
||||
throttleFailureCount: number;
|
||||
|
||||
@Column({ type: 'timestamptz', nullable: true })
|
||||
throttleRetryAfter: Date | null;
|
||||
|
||||
@Column({ type: 'uuid', nullable: false })
|
||||
connectedAccountId: string;
|
||||
|
||||
@ManyToOne(
|
||||
() => ConnectedAccountEntity,
|
||||
(connectedAccount) => connectedAccount.messageChannels,
|
||||
{ onDelete: 'CASCADE' },
|
||||
)
|
||||
@JoinColumn({ name: 'connectedAccountId' })
|
||||
connectedAccount: Relation<ConnectedAccountEntity>;
|
||||
|
||||
@OneToMany(
|
||||
'MessageFolderEntity',
|
||||
(messageFolder: MessageFolderEntity) => messageFolder.messageChannel,
|
||||
)
|
||||
messageFolders: Relation<MessageFolderEntity[]>;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
type CallHandler,
|
||||
type ExecutionContext,
|
||||
Injectable,
|
||||
type NestInterceptor,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { type Observable, catchError } from 'rxjs';
|
||||
|
||||
import { messageChannelGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/message-channel/utils/message-channel-graphql-api-exception-handler.util';
|
||||
|
||||
@Injectable()
|
||||
export class MessageChannelGraphqlApiExceptionInterceptor
|
||||
implements NestInterceptor
|
||||
{
|
||||
intercept(
|
||||
_context: ExecutionContext,
|
||||
next: CallHandler,
|
||||
): Observable<unknown> {
|
||||
return next
|
||||
.handle()
|
||||
.pipe(catchError(messageChannelGraphqlApiExceptionHandler));
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.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 { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([MessageChannelEntity]),
|
||||
AuthModule,
|
||||
PermissionsModule,
|
||||
FeatureFlagModule,
|
||||
],
|
||||
providers: [
|
||||
MessageChannelMetadataService,
|
||||
MessageChannelResolver,
|
||||
MessageChannelGraphqlApiExceptionInterceptor,
|
||||
],
|
||||
exports: [MessageChannelMetadataService],
|
||||
})
|
||||
export class MessageChannelMetadataModule {}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
MessageChannelSyncStage,
|
||||
MessageChannelType,
|
||||
MessageChannelVisibility,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { MessageChannelDTO } from 'src/engine/metadata-modules/message-channel/dtos/message-channel.dto';
|
||||
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
|
||||
@Injectable()
|
||||
export class MessageChannelMetadataService {
|
||||
constructor(
|
||||
@InjectRepository(MessageChannelEntity)
|
||||
private readonly repository: Repository<MessageChannelEntity>,
|
||||
) {}
|
||||
|
||||
async findAll(workspaceId: string): Promise<MessageChannelDTO[]> {
|
||||
return this.repository.find({ where: { workspaceId } });
|
||||
}
|
||||
|
||||
async findByConnectedAccountId(
|
||||
connectedAccountId: string,
|
||||
workspaceId: string,
|
||||
): Promise<MessageChannelDTO[]> {
|
||||
return this.repository.find({
|
||||
where: { connectedAccountId, workspaceId },
|
||||
});
|
||||
}
|
||||
|
||||
async findById(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
): Promise<MessageChannelDTO | null> {
|
||||
return this.repository.findOne({ where: { id, workspaceId } });
|
||||
}
|
||||
|
||||
async create(
|
||||
data: Partial<MessageChannelEntity> & {
|
||||
workspaceId: string;
|
||||
handle: string;
|
||||
connectedAccountId: string;
|
||||
visibility: MessageChannelVisibility;
|
||||
type: MessageChannelType;
|
||||
syncStage: MessageChannelSyncStage;
|
||||
},
|
||||
): Promise<MessageChannelDTO> {
|
||||
const entity = this.repository.create(data);
|
||||
|
||||
return this.repository.save(entity);
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
data: Partial<MessageChannelEntity>,
|
||||
): Promise<MessageChannelDTO> {
|
||||
await this.repository.update(
|
||||
{ id, workspaceId },
|
||||
data as Record<string, unknown>,
|
||||
);
|
||||
|
||||
return this.repository.findOneOrFail({ where: { id, workspaceId } });
|
||||
}
|
||||
|
||||
async delete(id: string, workspaceId: string): Promise<MessageChannelDTO> {
|
||||
const entity = await this.repository.findOneOrFail({
|
||||
where: { id, workspaceId },
|
||||
});
|
||||
|
||||
await this.repository.delete({ id, workspaceId });
|
||||
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { type MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export enum MessageChannelExceptionCode {
|
||||
MESSAGE_CHANNEL_NOT_FOUND = 'MESSAGE_CHANNEL_NOT_FOUND',
|
||||
INVALID_MESSAGE_CHANNEL_INPUT = 'INVALID_MESSAGE_CHANNEL_INPUT',
|
||||
}
|
||||
|
||||
const getMessageChannelExceptionUserFriendlyMessage = (
|
||||
code: MessageChannelExceptionCode,
|
||||
) => {
|
||||
switch (code) {
|
||||
case MessageChannelExceptionCode.MESSAGE_CHANNEL_NOT_FOUND:
|
||||
return msg`Message channel not found.`;
|
||||
case MessageChannelExceptionCode.INVALID_MESSAGE_CHANNEL_INPUT:
|
||||
return msg`Invalid message channel input.`;
|
||||
default:
|
||||
assertUnreachable(code);
|
||||
}
|
||||
};
|
||||
|
||||
export class MessageChannelException extends CustomException<MessageChannelExceptionCode> {
|
||||
constructor(
|
||||
message: string,
|
||||
code: MessageChannelExceptionCode,
|
||||
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
|
||||
) {
|
||||
super(message, code, {
|
||||
userFriendlyMessage:
|
||||
userFriendlyMessage ??
|
||||
getMessageChannelExceptionUserFriendlyMessage(code),
|
||||
});
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
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 { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import {
|
||||
FeatureFlagGuard,
|
||||
RequireFeatureFlag,
|
||||
} from 'src/engine/guards/feature-flag.guard';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { CreateMessageChannelInput } from 'src/engine/metadata-modules/message-channel/dtos/create-message-channel.input';
|
||||
import { MessageChannelDTO } from 'src/engine/metadata-modules/message-channel/dtos/message-channel.dto';
|
||||
import { UpdateMessageChannelInput } from 'src/engine/metadata-modules/message-channel/dtos/update-message-channel.input';
|
||||
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';
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard, FeatureFlagGuard)
|
||||
@UseInterceptors(MessageChannelGraphqlApiExceptionInterceptor)
|
||||
@MetadataResolver(() => MessageChannelDTO)
|
||||
export class MessageChannelResolver {
|
||||
constructor(
|
||||
private readonly messageChannelMetadataService: MessageChannelMetadataService,
|
||||
) {}
|
||||
|
||||
@Query(() => [MessageChannelDTO])
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async messageChannels(
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@Args('connectedAccountId', {
|
||||
type: () => UUIDScalarType,
|
||||
nullable: true,
|
||||
})
|
||||
connectedAccountId?: string,
|
||||
): Promise<MessageChannelDTO[]> {
|
||||
if (connectedAccountId) {
|
||||
return this.messageChannelMetadataService.findByConnectedAccountId(
|
||||
connectedAccountId,
|
||||
workspace.id,
|
||||
);
|
||||
}
|
||||
|
||||
return this.messageChannelMetadataService.findAll(workspace.id);
|
||||
}
|
||||
|
||||
@Query(() => MessageChannelDTO, { nullable: true })
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async messageChannel(
|
||||
@Args('id', { type: () => UUIDScalarType }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<MessageChannelDTO | null> {
|
||||
return this.messageChannelMetadataService.findById(id, workspace.id);
|
||||
}
|
||||
|
||||
@Mutation(() => MessageChannelDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async createMessageChannel(
|
||||
@Args('input') input: CreateMessageChannelInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<MessageChannelDTO> {
|
||||
return this.messageChannelMetadataService.create({
|
||||
...input,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => MessageChannelDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async updateMessageChannel(
|
||||
@Args('input') input: UpdateMessageChannelInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<MessageChannelDTO> {
|
||||
return this.messageChannelMetadataService.update(
|
||||
input.id,
|
||||
workspace.id,
|
||||
input.update,
|
||||
);
|
||||
}
|
||||
|
||||
@Mutation(() => MessageChannelDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async deleteMessageChannel(
|
||||
@Args('id', { type: () => UUIDScalarType }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<MessageChannelDTO> {
|
||||
return this.messageChannelMetadataService.delete(id, workspace.id);
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
NotFoundError,
|
||||
UserInputError,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import {
|
||||
MessageChannelException,
|
||||
MessageChannelExceptionCode,
|
||||
} from 'src/engine/metadata-modules/message-channel/message-channel.exception';
|
||||
|
||||
export const messageChannelGraphqlApiExceptionHandler = (error: Error) => {
|
||||
if (error instanceof MessageChannelException) {
|
||||
switch (error.code) {
|
||||
case MessageChannelExceptionCode.MESSAGE_CHANNEL_NOT_FOUND:
|
||||
throw new NotFoundError(error);
|
||||
case MessageChannelExceptionCode.INVALID_MESSAGE_CHANNEL_INPUT:
|
||||
throw new UserInputError(error);
|
||||
default: {
|
||||
return assertUnreachable(error.code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
};
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
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 {}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
await workspaceRepository.save(data);
|
||||
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
try {
|
||||
await this.coreRepository.save({
|
||||
...data,
|
||||
workspaceId,
|
||||
} 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 {
|
||||
await this.coreRepository.update(
|
||||
{ ...where, workspaceId } as FindOptionsWhere<MessageFolderEntity>,
|
||||
data as Record<string, unknown>,
|
||||
);
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
import { MessageFolderPendingSyncAction } from 'twenty-shared/types';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@InputType()
|
||||
export class CreateMessageFolderInput {
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
id?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
name?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
isSentFolder: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
isSynced: boolean;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
externalId?: string;
|
||||
|
||||
@IsEnum(MessageFolderPendingSyncAction)
|
||||
@IsNotEmpty()
|
||||
@Field(() => MessageFolderPendingSyncAction)
|
||||
pendingSyncAction: MessageFolderPendingSyncAction;
|
||||
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
@Field(() => UUIDScalarType)
|
||||
messageChannelId: string;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
parentFolderId?: string;
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import { Field, HideField, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
IsBoolean,
|
||||
IsDateString,
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
import { MessageFolderPendingSyncAction } from 'twenty-shared/types';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@ObjectType('MessageFolder')
|
||||
export class MessageFolderDTO {
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
@Field(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field(() => String, { nullable: true })
|
||||
name: string | null;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field(() => String, { nullable: true })
|
||||
syncCursor: string | null;
|
||||
|
||||
@IsBoolean()
|
||||
@Field()
|
||||
isSentFolder: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@Field()
|
||||
isSynced: boolean;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
parentFolderId: string | null;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field(() => String, { nullable: true })
|
||||
externalId: string | null;
|
||||
|
||||
@IsEnum(MessageFolderPendingSyncAction)
|
||||
@IsNotEmpty()
|
||||
@Field(() => MessageFolderPendingSyncAction)
|
||||
pendingSyncAction: MessageFolderPendingSyncAction;
|
||||
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
@Field(() => UUIDScalarType)
|
||||
messageChannelId: string;
|
||||
|
||||
@HideField()
|
||||
workspaceId: string;
|
||||
|
||||
@IsDateString()
|
||||
@Field()
|
||||
createdAt: Date;
|
||||
|
||||
@IsDateString()
|
||||
@Field()
|
||||
updatedAt: Date;
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { MessageFolderPendingSyncAction } from 'twenty-shared/types';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@InputType()
|
||||
export class UpdateMessageFolderInputUpdates {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
syncCursor?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Field({ nullable: true })
|
||||
isSynced?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(MessageFolderPendingSyncAction)
|
||||
@Field(() => MessageFolderPendingSyncAction, { nullable: true })
|
||||
pendingSyncAction?: MessageFolderPendingSyncAction;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
export class UpdateMessageFolderInput {
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
@Field(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@Type(() => UpdateMessageFolderInputUpdates)
|
||||
@ValidateNested()
|
||||
@Field(() => UpdateMessageFolderInputUpdates)
|
||||
update: UpdateMessageFolderInputUpdates;
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
type Relation,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import { MessageFolderPendingSyncAction } from 'twenty-shared/types';
|
||||
|
||||
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/workspace-related-entity';
|
||||
|
||||
@Entity({ name: 'messageFolder', schema: 'core' })
|
||||
export class MessageFolderEntity extends WorkspaceRelatedEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
name: string | null;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
syncCursor: string | null;
|
||||
|
||||
@Column({ type: 'boolean', nullable: false })
|
||||
isSentFolder: boolean;
|
||||
|
||||
@Column({ type: 'boolean', nullable: false })
|
||||
isSynced: boolean;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
parentFolderId: string | null;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
externalId: string | null;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: MessageFolderPendingSyncAction,
|
||||
nullable: false,
|
||||
})
|
||||
pendingSyncAction: MessageFolderPendingSyncAction;
|
||||
|
||||
@Column({ type: 'uuid', nullable: false })
|
||||
messageChannelId: string;
|
||||
|
||||
@ManyToOne(
|
||||
() => MessageChannelEntity,
|
||||
(messageChannel) => messageChannel.messageFolders,
|
||||
{ onDelete: 'CASCADE' },
|
||||
)
|
||||
@JoinColumn({ name: 'messageChannelId' })
|
||||
messageChannel: Relation<MessageChannelEntity>;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
type CallHandler,
|
||||
type ExecutionContext,
|
||||
Injectable,
|
||||
type NestInterceptor,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { type Observable, catchError } from 'rxjs';
|
||||
|
||||
import { messageFolderGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/message-folder/utils/message-folder-graphql-api-exception-handler.util';
|
||||
|
||||
@Injectable()
|
||||
export class MessageFolderGraphqlApiExceptionInterceptor
|
||||
implements NestInterceptor
|
||||
{
|
||||
intercept(
|
||||
_context: ExecutionContext,
|
||||
next: CallHandler,
|
||||
): Observable<unknown> {
|
||||
return next
|
||||
.handle()
|
||||
.pipe(catchError(messageFolderGraphqlApiExceptionHandler));
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
|
||||
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';
|
||||
import { MessageFolderResolver } from 'src/engine/metadata-modules/message-folder/resolvers/message-folder.resolver';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([MessageFolderEntity]),
|
||||
AuthModule,
|
||||
PermissionsModule,
|
||||
FeatureFlagModule,
|
||||
],
|
||||
providers: [
|
||||
MessageFolderMetadataService,
|
||||
MessageFolderResolver,
|
||||
MessageFolderGraphqlApiExceptionInterceptor,
|
||||
],
|
||||
exports: [MessageFolderMetadataService],
|
||||
})
|
||||
export class MessageFolderMetadataModule {}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { MessageFolderPendingSyncAction } from 'twenty-shared/types';
|
||||
|
||||
import { MessageFolderDTO } from 'src/engine/metadata-modules/message-folder/dtos/message-folder.dto';
|
||||
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
|
||||
|
||||
@Injectable()
|
||||
export class MessageFolderMetadataService {
|
||||
constructor(
|
||||
@InjectRepository(MessageFolderEntity)
|
||||
private readonly repository: Repository<MessageFolderEntity>,
|
||||
) {}
|
||||
|
||||
async findAll(workspaceId: string): Promise<MessageFolderDTO[]> {
|
||||
return this.repository.find({ where: { workspaceId } });
|
||||
}
|
||||
|
||||
async findByMessageChannelId(
|
||||
messageChannelId: string,
|
||||
workspaceId: string,
|
||||
): Promise<MessageFolderDTO[]> {
|
||||
return this.repository.find({
|
||||
where: { messageChannelId, workspaceId },
|
||||
});
|
||||
}
|
||||
|
||||
async findById(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
): Promise<MessageFolderDTO | null> {
|
||||
return this.repository.findOne({ where: { id, workspaceId } });
|
||||
}
|
||||
|
||||
async create(
|
||||
data: Partial<MessageFolderEntity> & {
|
||||
workspaceId: string;
|
||||
messageChannelId: string;
|
||||
pendingSyncAction: MessageFolderPendingSyncAction;
|
||||
},
|
||||
): Promise<MessageFolderDTO> {
|
||||
const entity = this.repository.create(data);
|
||||
|
||||
return this.repository.save(entity);
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
data: Partial<MessageFolderEntity>,
|
||||
): Promise<MessageFolderDTO> {
|
||||
await this.repository.update(
|
||||
{ id, workspaceId },
|
||||
data as Record<string, unknown>,
|
||||
);
|
||||
|
||||
return this.repository.findOneOrFail({ where: { id, workspaceId } });
|
||||
}
|
||||
|
||||
async delete(id: string, workspaceId: string): Promise<MessageFolderDTO> {
|
||||
const entity = await this.repository.findOneOrFail({
|
||||
where: { id, workspaceId },
|
||||
});
|
||||
|
||||
await this.repository.delete({ id, workspaceId });
|
||||
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { type MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export enum MessageFolderExceptionCode {
|
||||
MESSAGE_FOLDER_NOT_FOUND = 'MESSAGE_FOLDER_NOT_FOUND',
|
||||
INVALID_MESSAGE_FOLDER_INPUT = 'INVALID_MESSAGE_FOLDER_INPUT',
|
||||
}
|
||||
|
||||
const getMessageFolderExceptionUserFriendlyMessage = (
|
||||
code: MessageFolderExceptionCode,
|
||||
) => {
|
||||
switch (code) {
|
||||
case MessageFolderExceptionCode.MESSAGE_FOLDER_NOT_FOUND:
|
||||
return msg`Message folder not found.`;
|
||||
case MessageFolderExceptionCode.INVALID_MESSAGE_FOLDER_INPUT:
|
||||
return msg`Invalid message folder input.`;
|
||||
default:
|
||||
assertUnreachable(code);
|
||||
}
|
||||
};
|
||||
|
||||
export class MessageFolderException extends CustomException<MessageFolderExceptionCode> {
|
||||
constructor(
|
||||
message: string,
|
||||
code: MessageFolderExceptionCode,
|
||||
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
|
||||
) {
|
||||
super(message, code, {
|
||||
userFriendlyMessage:
|
||||
userFriendlyMessage ??
|
||||
getMessageFolderExceptionUserFriendlyMessage(code),
|
||||
});
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
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 { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import {
|
||||
FeatureFlagGuard,
|
||||
RequireFeatureFlag,
|
||||
} from 'src/engine/guards/feature-flag.guard';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { CreateMessageFolderInput } from 'src/engine/metadata-modules/message-folder/dtos/create-message-folder.input';
|
||||
import { MessageFolderDTO } from 'src/engine/metadata-modules/message-folder/dtos/message-folder.dto';
|
||||
import { UpdateMessageFolderInput } from 'src/engine/metadata-modules/message-folder/dtos/update-message-folder.input';
|
||||
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)
|
||||
@UseInterceptors(MessageFolderGraphqlApiExceptionInterceptor)
|
||||
@MetadataResolver(() => MessageFolderDTO)
|
||||
export class MessageFolderResolver {
|
||||
constructor(
|
||||
private readonly messageFolderMetadataService: MessageFolderMetadataService,
|
||||
) {}
|
||||
|
||||
@Query(() => [MessageFolderDTO])
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async messageFolders(
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@Args('messageChannelId', {
|
||||
type: () => UUIDScalarType,
|
||||
nullable: true,
|
||||
})
|
||||
messageChannelId?: string,
|
||||
): Promise<MessageFolderDTO[]> {
|
||||
if (messageChannelId) {
|
||||
return this.messageFolderMetadataService.findByMessageChannelId(
|
||||
messageChannelId,
|
||||
workspace.id,
|
||||
);
|
||||
}
|
||||
|
||||
return this.messageFolderMetadataService.findAll(workspace.id);
|
||||
}
|
||||
|
||||
@Query(() => MessageFolderDTO, { nullable: true })
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async messageFolder(
|
||||
@Args('id', { type: () => UUIDScalarType }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<MessageFolderDTO | null> {
|
||||
return this.messageFolderMetadataService.findById(id, workspace.id);
|
||||
}
|
||||
|
||||
@Mutation(() => MessageFolderDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async createMessageFolder(
|
||||
@Args('input') input: CreateMessageFolderInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<MessageFolderDTO> {
|
||||
return this.messageFolderMetadataService.create({
|
||||
...input,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => MessageFolderDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async updateMessageFolder(
|
||||
@Args('input') input: UpdateMessageFolderInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<MessageFolderDTO> {
|
||||
return this.messageFolderMetadataService.update(
|
||||
input.id,
|
||||
workspace.id,
|
||||
input.update,
|
||||
);
|
||||
}
|
||||
|
||||
@Mutation(() => MessageFolderDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async deleteMessageFolder(
|
||||
@Args('id', { type: () => UUIDScalarType }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<MessageFolderDTO> {
|
||||
return this.messageFolderMetadataService.delete(id, workspace.id);
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
NotFoundError,
|
||||
UserInputError,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import {
|
||||
MessageFolderException,
|
||||
MessageFolderExceptionCode,
|
||||
} from 'src/engine/metadata-modules/message-folder/message-folder.exception';
|
||||
|
||||
export const messageFolderGraphqlApiExceptionHandler = (error: Error) => {
|
||||
if (error instanceof MessageFolderException) {
|
||||
switch (error.code) {
|
||||
case MessageFolderExceptionCode.MESSAGE_FOLDER_NOT_FOUND:
|
||||
throw new NotFoundError(error);
|
||||
case MessageFolderExceptionCode.INVALID_MESSAGE_FOLDER_INPUT:
|
||||
throw new UserInputError(error);
|
||||
default: {
|
||||
return assertUnreachable(error.code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
};
|
||||
@@ -5,6 +5,8 @@ import { AiAgentMonitorModule } from 'src/engine/metadata-modules/ai/ai-agent-mo
|
||||
import { AiAgentModule } from 'src/engine/metadata-modules/ai/ai-agent/ai-agent.module';
|
||||
import { AiChatModule } from 'src/engine/metadata-modules/ai/ai-chat/ai-chat.module';
|
||||
import { AiGenerateTextModule } from 'src/engine/metadata-modules/ai/ai-generate-text/ai-generate-text.module';
|
||||
import { CalendarChannelMetadataModule } from 'src/engine/metadata-modules/calendar-channel/calendar-channel-metadata.module';
|
||||
import { ConnectedAccountMetadataModule } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.module';
|
||||
import { CommandMenuItemModule } from 'src/engine/metadata-modules/command-menu-item/command-menu-item.module';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
|
||||
@@ -12,6 +14,8 @@ import { FlatEntityMapsGraphqlApiExceptionFilter } from 'src/engine/metadata-mod
|
||||
import { FrontComponentModule } from 'src/engine/metadata-modules/front-component/front-component.module';
|
||||
import { LogicFunctionLayerModule } from 'src/engine/metadata-modules/logic-function-layer/logic-function-layer.module';
|
||||
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
|
||||
import { MessageChannelMetadataModule } from 'src/engine/metadata-modules/message-channel/message-channel-metadata.module';
|
||||
import { MessageFolderMetadataModule } from 'src/engine/metadata-modules/message-folder/message-folder-metadata.module';
|
||||
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 { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
@@ -47,6 +51,10 @@ import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/work
|
||||
PermissionsModule,
|
||||
RouteTriggerModule,
|
||||
WebhookModule,
|
||||
ConnectedAccountMetadataModule,
|
||||
MessageChannelMetadataModule,
|
||||
CalendarChannelMetadataModule,
|
||||
MessageFolderMetadataModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
@@ -71,6 +79,10 @@ import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/work
|
||||
RoleModule,
|
||||
PermissionsModule,
|
||||
WebhookModule,
|
||||
ConnectedAccountMetadataModule,
|
||||
MessageChannelMetadataModule,
|
||||
CalendarChannelMetadataModule,
|
||||
MessageFolderMetadataModule,
|
||||
],
|
||||
})
|
||||
export class MetadataEngineModule {}
|
||||
|
||||
+1
@@ -250,6 +250,7 @@ describe('WorkspaceEntityManager', () => {
|
||||
IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED: false,
|
||||
IS_DRAFT_EMAIL_ENABLED: false,
|
||||
IS_RICH_TEXT_V1_MIGRATED: false,
|
||||
IS_CONNECTED_ACCOUNT_MIGRATED: false,
|
||||
},
|
||||
userWorkspaceRoleMap: {},
|
||||
eventEmitterService: {
|
||||
|
||||
+6
-1
@@ -1,5 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { CalendarChannelDataAccessModule } from 'src/engine/metadata-modules/calendar-channel/data-access/calendar-channel-data-access.module';
|
||||
import { BlocklistItemDeleteCalendarEventsJob } from 'src/modules/calendar/blocklist-manager/jobs/blocklist-item-delete-calendar-events.job';
|
||||
import { BlocklistReimportCalendarEventsJob } from 'src/modules/calendar/blocklist-manager/jobs/blocklist-reimport-calendar-events.job';
|
||||
import { CalendarBlocklistListener } from 'src/modules/calendar/blocklist-manager/listeners/calendar-blocklist.listener';
|
||||
@@ -7,7 +8,11 @@ import { CalendarEventCleanerModule } from 'src/modules/calendar/calendar-event-
|
||||
import { CalendarCommonModule } from 'src/modules/calendar/common/calendar-common.module';
|
||||
|
||||
@Module({
|
||||
imports: [CalendarEventCleanerModule, CalendarCommonModule],
|
||||
imports: [
|
||||
CalendarEventCleanerModule,
|
||||
CalendarCommonModule,
|
||||
CalendarChannelDataAccessModule,
|
||||
],
|
||||
providers: [
|
||||
CalendarBlocklistListener,
|
||||
BlocklistItemDeleteCalendarEventsJob,
|
||||
|
||||
+24
-23
@@ -7,13 +7,13 @@ import { type ObjectRecordCreateEvent } from 'twenty-shared/database-events';
|
||||
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
|
||||
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-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 { type WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
|
||||
import { type BlocklistWorkspaceEntity } from 'src/modules/blocklist/standard-objects/blocklist.workspace-entity';
|
||||
import { CalendarEventCleanerService } from 'src/modules/calendar/calendar-event-cleaner/services/calendar-event-cleaner.service';
|
||||
import { type CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity';
|
||||
import { type CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
|
||||
export type BlocklistItemDeleteCalendarEventsJobData = WorkspaceEventBatch<
|
||||
ObjectRecordCreateEvent<BlocklistWorkspaceEntity>
|
||||
@@ -26,6 +26,7 @@ export type BlocklistItemDeleteCalendarEventsJobData = WorkspaceEventBatch<
|
||||
export class BlocklistItemDeleteCalendarEventsJob {
|
||||
constructor(
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService,
|
||||
private readonly calendarEventCleanerService: CalendarEventCleanerService,
|
||||
) {}
|
||||
|
||||
@@ -71,12 +72,6 @@ export class BlocklistItemDeleteCalendarEventsJob {
|
||||
new Map<string, string[]>(),
|
||||
);
|
||||
|
||||
const calendarChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'calendarChannel',
|
||||
);
|
||||
|
||||
const calendarChannelEventAssociationRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<CalendarChannelEventAssociationWorkspaceEntity>(
|
||||
workspaceId,
|
||||
@@ -91,29 +86,35 @@ export class BlocklistItemDeleteCalendarEventsJob {
|
||||
continue;
|
||||
}
|
||||
|
||||
const calendarChannels = await calendarChannelRepository.find({
|
||||
select: {
|
||||
id: true,
|
||||
handle: true,
|
||||
connectedAccount: {
|
||||
handleAliases: true,
|
||||
const calendarChannels =
|
||||
await this.calendarChannelDataAccessService.find(workspaceId, {
|
||||
select: {
|
||||
id: true,
|
||||
handle: true,
|
||||
connectedAccount: {
|
||||
handleAliases: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
connectedAccount: {
|
||||
accountOwnerId: workspaceMemberId,
|
||||
where: {
|
||||
connectedAccount: {
|
||||
accountOwnerId: workspaceMemberId,
|
||||
},
|
||||
},
|
||||
},
|
||||
relations: ['connectedAccount'],
|
||||
});
|
||||
relations: ['connectedAccount'],
|
||||
});
|
||||
|
||||
for (const calendarChannel of calendarChannels) {
|
||||
const calendarChannelHandles = [calendarChannel.handle];
|
||||
|
||||
if (calendarChannel.connectedAccount.handleAliases) {
|
||||
calendarChannelHandles.push(
|
||||
...calendarChannel.connectedAccount.handleAliases.split(','),
|
||||
);
|
||||
const rawAliases = calendarChannel.connectedAccount
|
||||
.handleAliases as string | string[];
|
||||
|
||||
const aliasList = Array.isArray(rawAliases)
|
||||
? rawAliases
|
||||
: rawAliases.split(',').map((alias: string) => alias.trim());
|
||||
|
||||
calendarChannelHandles.push(...aliasList);
|
||||
}
|
||||
|
||||
const handleConditions = handles.map((handle) => {
|
||||
|
||||
+14
-20
@@ -6,15 +6,13 @@ import { type ObjectRecordDeleteEvent } from 'twenty-shared/database-events';
|
||||
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
|
||||
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-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 { type WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
|
||||
import { type BlocklistWorkspaceEntity } from 'src/modules/blocklist/standard-objects/blocklist.workspace-entity';
|
||||
import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service';
|
||||
import {
|
||||
CalendarChannelSyncStage,
|
||||
type CalendarChannelWorkspaceEntity,
|
||||
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
import { CalendarChannelSyncStage } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
|
||||
export type BlocklistReimportCalendarEventsJobData = WorkspaceEventBatch<
|
||||
ObjectRecordDeleteEvent<BlocklistWorkspaceEntity>
|
||||
@@ -27,6 +25,7 @@ export type BlocklistReimportCalendarEventsJobData = WorkspaceEventBatch<
|
||||
export class BlocklistReimportCalendarEventsJob {
|
||||
constructor(
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService,
|
||||
private readonly calendarChannelSyncStatusService: CalendarChannelSyncStatusService,
|
||||
) {}
|
||||
|
||||
@@ -37,27 +36,22 @@ export class BlocklistReimportCalendarEventsJob {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const calendarChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'calendarChannel',
|
||||
);
|
||||
|
||||
for (const eventPayload of data.events) {
|
||||
const workspaceMemberId =
|
||||
eventPayload.properties.before.workspaceMemberId;
|
||||
|
||||
const calendarChannels = await calendarChannelRepository.find({
|
||||
select: ['id'],
|
||||
where: {
|
||||
connectedAccount: {
|
||||
accountOwnerId: workspaceMemberId,
|
||||
const calendarChannels =
|
||||
await this.calendarChannelDataAccessService.find(workspaceId, {
|
||||
select: ['id'],
|
||||
where: {
|
||||
connectedAccount: {
|
||||
accountOwnerId: workspaceMemberId,
|
||||
},
|
||||
syncStage: Not(
|
||||
CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING,
|
||||
),
|
||||
},
|
||||
syncStage: Not(
|
||||
CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING,
|
||||
),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
await this.calendarChannelSyncStatusService.resetAndMarkAsCalendarEventListFetchPending(
|
||||
calendarChannels.map((calendarChannel) => calendarChannel.id),
|
||||
|
||||
+4
@@ -6,6 +6,8 @@ import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-
|
||||
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
||||
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 { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
import { BlocklistWorkspaceEntity } from 'src/modules/blocklist/standard-objects/blocklist.workspace-entity';
|
||||
@@ -57,6 +59,8 @@ import { RefreshTokensManagerModule } from 'src/modules/connected-account/refres
|
||||
ConnectedAccountModule,
|
||||
CalendarCommonModule,
|
||||
MetricsModule,
|
||||
CalendarChannelDataAccessModule,
|
||||
ConnectedAccountDataAccessModule,
|
||||
],
|
||||
providers: [
|
||||
CalendarAccountAuthenticationService,
|
||||
|
||||
+23
-26
@@ -5,16 +5,14 @@ import { Command, CommandRunner, Option } from 'nest-commander';
|
||||
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 { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-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 {
|
||||
CalendarEventListFetchJob,
|
||||
type CalendarEventListFetchJobData,
|
||||
} from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job';
|
||||
import {
|
||||
CalendarChannelSyncStage,
|
||||
type CalendarChannelWorkspaceEntity,
|
||||
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
import { CalendarChannelSyncStage } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
|
||||
type CalendarTriggerEventListFetchCommandOptions = {
|
||||
workspaceId: string;
|
||||
@@ -33,6 +31,7 @@ export class CalendarTriggerEventListFetchCommand extends CommandRunner {
|
||||
|
||||
constructor(
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService,
|
||||
@InjectMessageQueue(MessageQueue.calendarQueue)
|
||||
private readonly messageQueueService: MessageQueueService,
|
||||
) {
|
||||
@@ -52,23 +51,17 @@ export class CalendarTriggerEventListFetchCommand extends CommandRunner {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const calendarChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'calendarChannel',
|
||||
);
|
||||
|
||||
const whereCondition: Record<string, unknown> = {
|
||||
isSyncEnabled: true,
|
||||
syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING,
|
||||
};
|
||||
|
||||
if (calendarChannelId) {
|
||||
whereCondition.id = calendarChannelId;
|
||||
}
|
||||
|
||||
const calendarChannels =
|
||||
await calendarChannelRepository.find(whereCondition);
|
||||
const calendarChannels = await this.calendarChannelDataAccessService.find(
|
||||
workspaceId,
|
||||
{
|
||||
where: {
|
||||
isSyncEnabled: true,
|
||||
syncStage:
|
||||
CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING,
|
||||
...(calendarChannelId ? { id: calendarChannelId } : {}),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (calendarChannels.length === 0) {
|
||||
this.logger.warn(
|
||||
@@ -83,11 +76,15 @@ export class CalendarTriggerEventListFetchCommand extends CommandRunner {
|
||||
);
|
||||
|
||||
for (const calendarChannel of calendarChannels) {
|
||||
await calendarChannelRepository.update(calendarChannel.id, {
|
||||
syncStage:
|
||||
CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED,
|
||||
syncStageStartedAt: new Date().toISOString(),
|
||||
});
|
||||
await this.calendarChannelDataAccessService.update(
|
||||
workspaceId,
|
||||
{ id: calendarChannel.id },
|
||||
{
|
||||
syncStage:
|
||||
CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED,
|
||||
syncStageStartedAt: new Date().toISOString(),
|
||||
},
|
||||
);
|
||||
|
||||
await this.messageQueueService.add<CalendarEventListFetchJobData>(
|
||||
CalendarEventListFetchJob.name,
|
||||
|
||||
+16
-19
@@ -3,6 +3,7 @@ import { Scope } from '@nestjs/common';
|
||||
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
|
||||
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-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 { CalendarFetchEventsService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-fetch-events.service';
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
CalendarChannelSyncStage,
|
||||
type CalendarChannelWorkspaceEntity,
|
||||
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { isThrottled } from 'src/modules/connected-account/utils/is-throttled';
|
||||
|
||||
export type CalendarEventListFetchJobData = {
|
||||
@@ -25,6 +27,7 @@ export type CalendarEventListFetchJobData = {
|
||||
export class CalendarEventListFetchJob {
|
||||
constructor(
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService,
|
||||
private readonly calendarChannelSyncStatusService: CalendarChannelSyncStatusService,
|
||||
private readonly calendarFetchEventsService: CalendarFetchEventsService,
|
||||
) {}
|
||||
@@ -36,19 +39,14 @@ export class CalendarEventListFetchJob {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const calendarChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'calendarChannel',
|
||||
);
|
||||
|
||||
const calendarChannel = await calendarChannelRepository.findOne({
|
||||
where: {
|
||||
id: calendarChannelId,
|
||||
isSyncEnabled: true,
|
||||
},
|
||||
relations: ['connectedAccount'],
|
||||
});
|
||||
const calendarChannel =
|
||||
await this.calendarChannelDataAccessService.findOne(workspaceId, {
|
||||
where: {
|
||||
id: calendarChannelId,
|
||||
isSyncEnabled: true,
|
||||
},
|
||||
relations: ['connectedAccount'],
|
||||
});
|
||||
|
||||
if (!calendarChannel) {
|
||||
return;
|
||||
@@ -61,11 +59,10 @@ export class CalendarEventListFetchJob {
|
||||
return;
|
||||
}
|
||||
|
||||
const syncStageStartedAt = calendarChannel.syncStageStartedAt;
|
||||
|
||||
if (
|
||||
isThrottled(
|
||||
calendarChannel.syncStageStartedAt,
|
||||
calendarChannel.throttleFailureCount,
|
||||
)
|
||||
isThrottled(syncStageStartedAt, calendarChannel.throttleFailureCount)
|
||||
) {
|
||||
await this.calendarChannelSyncStatusService.markAsCalendarEventListFetchPending(
|
||||
[calendarChannel.id],
|
||||
@@ -77,8 +74,8 @@ export class CalendarEventListFetchJob {
|
||||
}
|
||||
|
||||
await this.calendarFetchEventsService.fetchCalendarEvents(
|
||||
calendarChannel,
|
||||
calendarChannel.connectedAccount,
|
||||
calendarChannel as unknown as CalendarChannelWorkspaceEntity,
|
||||
calendarChannel.connectedAccount as unknown as ConnectedAccountWorkspaceEntity,
|
||||
workspaceId,
|
||||
);
|
||||
}, authContext);
|
||||
|
||||
+16
-18
@@ -3,6 +3,7 @@ import { Scope } from '@nestjs/common';
|
||||
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
|
||||
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-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 { CalendarEventsImportService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-events-import.service';
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
CalendarChannelSyncStage,
|
||||
type CalendarChannelWorkspaceEntity,
|
||||
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { isThrottled } from 'src/modules/connected-account/utils/is-throttled';
|
||||
|
||||
export type CalendarEventsImportJobData = {
|
||||
@@ -27,6 +29,7 @@ export class CalendarEventsImportJob {
|
||||
private readonly calendarEventsImportService: CalendarEventsImportService,
|
||||
private readonly calendarChannelSyncStatusService: CalendarChannelSyncStatusService,
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService,
|
||||
) {}
|
||||
|
||||
@Process(CalendarEventsImportJob.name)
|
||||
@@ -36,18 +39,14 @@ export class CalendarEventsImportJob {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const calendarChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'calendarChannel',
|
||||
);
|
||||
const calendarChannel = await calendarChannelRepository.findOne({
|
||||
where: {
|
||||
id: calendarChannelId,
|
||||
isSyncEnabled: true,
|
||||
},
|
||||
relations: ['connectedAccount'],
|
||||
});
|
||||
const calendarChannel =
|
||||
await this.calendarChannelDataAccessService.findOne(workspaceId, {
|
||||
where: {
|
||||
id: calendarChannelId,
|
||||
isSyncEnabled: true,
|
||||
},
|
||||
relations: ['connectedAccount'],
|
||||
});
|
||||
|
||||
if (!calendarChannel?.isSyncEnabled) {
|
||||
return;
|
||||
@@ -60,11 +59,10 @@ export class CalendarEventsImportJob {
|
||||
return;
|
||||
}
|
||||
|
||||
const syncStageStartedAt = calendarChannel.syncStageStartedAt;
|
||||
|
||||
if (
|
||||
isThrottled(
|
||||
calendarChannel.syncStageStartedAt,
|
||||
calendarChannel.throttleFailureCount,
|
||||
)
|
||||
isThrottled(syncStageStartedAt, calendarChannel.throttleFailureCount)
|
||||
) {
|
||||
await this.calendarChannelSyncStatusService.markAsCalendarEventsImportPending(
|
||||
[calendarChannel.id],
|
||||
@@ -76,8 +74,8 @@ export class CalendarEventsImportJob {
|
||||
}
|
||||
|
||||
await this.calendarEventsImportService.processCalendarEventsImport(
|
||||
calendarChannel,
|
||||
calendarChannel.connectedAccount,
|
||||
calendarChannel as unknown as CalendarChannelWorkspaceEntity,
|
||||
calendarChannel.connectedAccount as unknown as ConnectedAccountWorkspaceEntity,
|
||||
workspaceId,
|
||||
);
|
||||
}, authContext);
|
||||
|
||||
+18
-20
@@ -5,14 +5,12 @@ import { In } from 'typeorm';
|
||||
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
|
||||
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-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 { isSyncStale } from 'src/modules/calendar/calendar-event-import-manager/utils/is-sync-stale.util';
|
||||
import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service';
|
||||
import {
|
||||
CalendarChannelSyncStage,
|
||||
type CalendarChannelWorkspaceEntity,
|
||||
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
import { CalendarChannelSyncStage } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
|
||||
export type CalendarOngoingStaleJobData = {
|
||||
workspaceId: string;
|
||||
@@ -26,6 +24,7 @@ export class CalendarOngoingStaleJob {
|
||||
private readonly logger = new Logger(CalendarOngoingStaleJob.name);
|
||||
constructor(
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService,
|
||||
private readonly calendarChannelSyncStatusService: CalendarChannelSyncStatusService,
|
||||
) {}
|
||||
|
||||
@@ -36,25 +35,24 @@ export class CalendarOngoingStaleJob {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const calendarChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'calendarChannel',
|
||||
);
|
||||
|
||||
const calendarChannels = await calendarChannelRepository.find({
|
||||
where: {
|
||||
syncStage: In([
|
||||
CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_ONGOING,
|
||||
CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_ONGOING,
|
||||
CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_SCHEDULED,
|
||||
CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED,
|
||||
]),
|
||||
const calendarChannels = await this.calendarChannelDataAccessService.find(
|
||||
workspaceId,
|
||||
{
|
||||
where: {
|
||||
syncStage: In([
|
||||
CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_ONGOING,
|
||||
CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_ONGOING,
|
||||
CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_SCHEDULED,
|
||||
CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED,
|
||||
]),
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
for (const calendarChannel of calendarChannels) {
|
||||
if (isSyncStale(calendarChannel.syncStageStartedAt)) {
|
||||
const syncStageStartedAt = calendarChannel.syncStageStartedAt;
|
||||
|
||||
if (isSyncStale(syncStageStartedAt)) {
|
||||
await this.calendarChannelSyncStatusService.resetSyncStageStartedAt(
|
||||
[calendarChannel.id],
|
||||
workspaceId,
|
||||
|
||||
+15
-21
@@ -3,12 +3,12 @@ import { Scope } from '@nestjs/common';
|
||||
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
|
||||
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-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 {
|
||||
CalendarChannelSyncStage,
|
||||
CalendarChannelSyncStatus,
|
||||
CalendarChannelWorkspaceEntity,
|
||||
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
|
||||
export type CalendarRelaunchFailedCalendarChannelJobData = {
|
||||
@@ -23,6 +23,7 @@ export type CalendarRelaunchFailedCalendarChannelJobData = {
|
||||
export class CalendarRelaunchFailedCalendarChannelJob {
|
||||
constructor(
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService,
|
||||
) {}
|
||||
|
||||
@Process(CalendarRelaunchFailedCalendarChannelJob.name)
|
||||
@@ -32,23 +33,12 @@ export class CalendarRelaunchFailedCalendarChannelJob {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const calendarChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'calendarChannel',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const calendarChannel = await calendarChannelRepository.findOne({
|
||||
where: {
|
||||
id: calendarChannelId,
|
||||
},
|
||||
relations: {
|
||||
connectedAccount: {
|
||||
accountOwner: true,
|
||||
const calendarChannel =
|
||||
await this.calendarChannelDataAccessService.findOne(workspaceId, {
|
||||
where: {
|
||||
id: calendarChannelId,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
if (
|
||||
!calendarChannel ||
|
||||
@@ -58,10 +48,14 @@ export class CalendarRelaunchFailedCalendarChannelJob {
|
||||
return;
|
||||
}
|
||||
|
||||
await calendarChannelRepository.update(calendarChannelId, {
|
||||
syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING,
|
||||
syncStatus: CalendarChannelSyncStatus.ACTIVE,
|
||||
});
|
||||
await this.calendarChannelDataAccessService.update(
|
||||
workspaceId,
|
||||
{ id: calendarChannelId },
|
||||
{
|
||||
syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING,
|
||||
syncStatus: CalendarChannelSyncStatus.ACTIVE,
|
||||
},
|
||||
);
|
||||
}, authContext);
|
||||
}
|
||||
}
|
||||
|
||||
+10
-22
@@ -1,12 +1,11 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service';
|
||||
import {
|
||||
type TwentyORMException,
|
||||
TwentyORMExceptionCode,
|
||||
} from 'src/engine/twenty-orm/exceptions/twenty-orm.exception';
|
||||
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 { CALENDAR_THROTTLE_MAX_ATTEMPTS } from 'src/modules/calendar/calendar-event-import-manager/constants/calendar-throttle-max-attempts';
|
||||
import {
|
||||
type CalendarEventImportDriverException,
|
||||
@@ -29,7 +28,7 @@ export class CalendarEventImportErrorHandlerService {
|
||||
CalendarEventImportErrorHandlerService.name,
|
||||
);
|
||||
constructor(
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService,
|
||||
private readonly calendarChannelSyncStatusService: CalendarChannelSyncStatusService,
|
||||
private readonly exceptionHandlerService: ExceptionHandlerService,
|
||||
) {}
|
||||
@@ -133,25 +132,14 @@ export class CalendarEventImportErrorHandlerService {
|
||||
throw calendarEventImportException;
|
||||
}
|
||||
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const calendarChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'calendarChannel',
|
||||
);
|
||||
|
||||
await calendarChannelRepository.increment(
|
||||
{
|
||||
id: calendarChannel.id,
|
||||
},
|
||||
'throttleFailureCount',
|
||||
1,
|
||||
undefined,
|
||||
['throttleFailureCount', 'id'],
|
||||
);
|
||||
}, authContext);
|
||||
await this.calendarChannelDataAccessService.increment(
|
||||
workspaceId,
|
||||
{
|
||||
id: calendarChannel.id,
|
||||
},
|
||||
'throttleFailureCount',
|
||||
1,
|
||||
);
|
||||
|
||||
switch (syncStep) {
|
||||
case CalendarEventImportSyncStep.CALENDAR_EVENT_LIST_FETCH:
|
||||
|
||||
+6
-8
@@ -5,6 +5,7 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
|
||||
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
|
||||
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
|
||||
import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-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 {
|
||||
@@ -29,6 +30,7 @@ export class CalendarFetchEventsService {
|
||||
@InjectCacheStorage(CacheStorageNamespace.ModuleCalendar)
|
||||
private readonly cacheStorage: CacheStorageService,
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService,
|
||||
private readonly calendarChannelSyncStatusService: CalendarChannelSyncStatusService,
|
||||
private readonly getCalendarEventsService: CalendarGetCalendarEventsService,
|
||||
private readonly calendarEventImportErrorHandlerService: CalendarEventImportErrorHandlerService,
|
||||
@@ -90,14 +92,9 @@ export class CalendarFetchEventsService {
|
||||
const calendarEventIds = getCalendarEventsResponse.calendarEventIds;
|
||||
const nextSyncCursor = getCalendarEventsResponse.nextSyncCursor;
|
||||
|
||||
const calendarChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'calendarChannel',
|
||||
);
|
||||
|
||||
if (!calendarEvents || calendarEvents?.length === 0) {
|
||||
await calendarChannelRepository.update(
|
||||
await this.calendarChannelDataAccessService.update(
|
||||
workspaceId,
|
||||
{
|
||||
id: calendarChannel.id,
|
||||
},
|
||||
@@ -112,7 +109,8 @@ export class CalendarFetchEventsService {
|
||||
);
|
||||
}
|
||||
|
||||
await calendarChannelRepository.update(
|
||||
await this.calendarChannelDataAccessService.update(
|
||||
workspaceId,
|
||||
{
|
||||
id: calendarChannel.id,
|
||||
},
|
||||
|
||||
@@ -3,6 +3,8 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.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 { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service';
|
||||
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
|
||||
@@ -12,6 +14,8 @@ import { ConnectedAccountModule } from 'src/modules/connected-account/connected-
|
||||
WorkspaceDataSourceModule,
|
||||
TypeOrmModule.forFeature([FeatureFlagEntity]),
|
||||
ConnectedAccountModule,
|
||||
CalendarChannelDataAccessModule,
|
||||
ConnectedAccountDataAccessModule,
|
||||
MetricsModule,
|
||||
],
|
||||
providers: [CalendarChannelSyncStatusService],
|
||||
|
||||
+15
-13
@@ -2,6 +2,7 @@ import { Test, type TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
|
||||
|
||||
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 { 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';
|
||||
@@ -44,22 +45,19 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
|
||||
find: jest.fn(),
|
||||
};
|
||||
|
||||
const mockConnectedAccountRepository = {
|
||||
find: jest.fn(),
|
||||
};
|
||||
|
||||
const mockWorkspaceMemberRepository = {
|
||||
findOneByOrFail: jest.fn(),
|
||||
};
|
||||
|
||||
const mockConnectedAccountDataAccessService = {
|
||||
find: jest.fn(),
|
||||
};
|
||||
|
||||
const mockGlobalWorkspaceOrmManager = {
|
||||
getRepository: jest.fn().mockImplementation((workspaceId, name) => {
|
||||
if (name === 'calendarChannelEventAssociation') {
|
||||
return mockCalendarEventAssociationRepository;
|
||||
}
|
||||
if (name === 'connectedAccount') {
|
||||
return mockConnectedAccountRepository;
|
||||
}
|
||||
if (name === 'workspaceMember') {
|
||||
return mockWorkspaceMemberRepository;
|
||||
}
|
||||
@@ -77,6 +75,10 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
|
||||
provide: GlobalWorkspaceOrmManager,
|
||||
useValue: mockGlobalWorkspaceOrmManager,
|
||||
},
|
||||
{
|
||||
provide: ConnectedAccountDataAccessService,
|
||||
useValue: mockConnectedAccountDataAccessService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@@ -121,7 +123,7 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
|
||||
item.description !== FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
|
||||
),
|
||||
).toBe(true);
|
||||
expect(mockConnectedAccountRepository.find).not.toHaveBeenCalled();
|
||||
expect(mockConnectedAccountDataAccessService.find).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return calendar event with obfuscated title and description if the visibility is METADATA', async () => {
|
||||
@@ -143,7 +145,7 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
|
||||
id: 'workspace-member-id',
|
||||
});
|
||||
|
||||
mockConnectedAccountRepository.find.mockResolvedValue([]);
|
||||
mockConnectedAccountDataAccessService.find.mockResolvedValue([]);
|
||||
|
||||
const result = await service.applyCalendarEventsVisibilityRestrictions(
|
||||
calendarEvents,
|
||||
@@ -179,7 +181,7 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
|
||||
id: 'workspace-member-account-owner-id',
|
||||
});
|
||||
|
||||
mockConnectedAccountRepository.find.mockResolvedValue([{ id: '1' }]);
|
||||
mockConnectedAccountDataAccessService.find.mockResolvedValue([{ id: '1' }]);
|
||||
|
||||
const result = await service.applyCalendarEventsVisibilityRestrictions(
|
||||
calendarEvents,
|
||||
@@ -215,7 +217,7 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
|
||||
id: 'workspace-member-not-account-owner-id',
|
||||
});
|
||||
|
||||
mockConnectedAccountRepository.find.mockResolvedValue([]);
|
||||
mockConnectedAccountDataAccessService.find.mockResolvedValue([]);
|
||||
|
||||
const result = await service.applyCalendarEventsVisibilityRestrictions(
|
||||
calendarEvents,
|
||||
@@ -261,7 +263,7 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
|
||||
},
|
||||
]);
|
||||
|
||||
mockConnectedAccountRepository.find
|
||||
mockConnectedAccountDataAccessService.find
|
||||
.mockResolvedValueOnce([]) // request for calendar event 3
|
||||
.mockResolvedValueOnce([{ id: '1' }]); // request for calendar event 2
|
||||
|
||||
@@ -317,7 +319,7 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
|
||||
},
|
||||
]);
|
||||
|
||||
mockConnectedAccountRepository.find
|
||||
mockConnectedAccountDataAccessService.find
|
||||
.mockResolvedValueOnce([]) // request for calendar event 3
|
||||
.mockResolvedValueOnce([{ id: '1' }]); // request for calendar event 2
|
||||
|
||||
|
||||
+5
-12
@@ -5,18 +5,19 @@ import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { In } from 'typeorm';
|
||||
|
||||
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 { type CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity';
|
||||
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 ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class ApplyCalendarEventsVisibilityRestrictionsService {
|
||||
constructor(
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
|
||||
) {}
|
||||
|
||||
public async applyCalendarEventsVisibilityRestrictions(
|
||||
@@ -42,12 +43,6 @@ export class ApplyCalendarEventsVisibilityRestrictionsService {
|
||||
relations: ['calendarChannel'],
|
||||
});
|
||||
|
||||
const connectedAccountRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'connectedAccount',
|
||||
);
|
||||
|
||||
const workspaceMemberRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
|
||||
workspaceId,
|
||||
@@ -84,15 +79,13 @@ export class ApplyCalendarEventsVisibilityRestrictionsService {
|
||||
userId: userId,
|
||||
});
|
||||
|
||||
const connectedAccounts = await connectedAccountRepository.find({
|
||||
select: ['id'],
|
||||
where: {
|
||||
const connectedAccounts =
|
||||
await this.connectedAccountDataAccessService.find(workspaceId, {
|
||||
calendarChannels: {
|
||||
id: In(calendarChannels.map((channel) => channel.id)),
|
||||
},
|
||||
accountOwnerId: workspaceMember.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
if (connectedAccounts.length > 0) {
|
||||
continue;
|
||||
|
||||
+2
@@ -1,10 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module';
|
||||
import { CalendarEventFindManyPostQueryHook } from 'src/modules/calendar/common/query-hooks/calendar-event/calendar-event-find-many.post-query.hook';
|
||||
import { CalendarEventFindOnePostQueryHook } from 'src/modules/calendar/common/query-hooks/calendar-event/calendar-event-find-one.post-query.hook';
|
||||
import { ApplyCalendarEventsVisibilityRestrictionsService } from 'src/modules/calendar/common/query-hooks/calendar-event/services/apply-calendar-events-visibility-restrictions.service';
|
||||
|
||||
@Module({
|
||||
imports: [ConnectedAccountDataAccessModule],
|
||||
providers: [
|
||||
ApplyCalendarEventsVisibilityRestrictionsService,
|
||||
CalendarEventFindOnePostQueryHook,
|
||||
|
||||
+125
-125
@@ -1,22 +1,23 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { Any } from 'typeorm';
|
||||
import { Any, In } from 'typeorm';
|
||||
|
||||
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
|
||||
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
|
||||
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
|
||||
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
|
||||
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
|
||||
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 { 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,
|
||||
type CalendarChannelWorkspaceEntity,
|
||||
} 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 { AccountsToReconnectKeys } from 'src/modules/connected-account/types/accounts-to-reconnect-key-value.type';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class CalendarChannelSyncStatusService {
|
||||
@@ -24,6 +25,8 @@ export class CalendarChannelSyncStatusService {
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
@InjectCacheStorage(CacheStorageNamespace.ModuleCalendar)
|
||||
private readonly cacheStorage: CacheStorageService,
|
||||
private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService,
|
||||
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
|
||||
private readonly accountsToReconnectService: AccountsToReconnectService,
|
||||
private readonly metricsService: MetricsService,
|
||||
) {}
|
||||
@@ -40,16 +43,14 @@ export class CalendarChannelSyncStatusService {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const calendarChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'calendarChannel',
|
||||
);
|
||||
|
||||
await calendarChannelRepository.update(calendarChannelIds, {
|
||||
syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING,
|
||||
...(!preserveSyncStageStartedAt ? { syncStageStartedAt: null } : {}),
|
||||
});
|
||||
await this.calendarChannelDataAccessService.update(
|
||||
workspaceId,
|
||||
{ id: In(calendarChannelIds) },
|
||||
{
|
||||
syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING,
|
||||
...(!preserveSyncStageStartedAt ? { syncStageStartedAt: null } : {}),
|
||||
},
|
||||
);
|
||||
}, authContext);
|
||||
}
|
||||
|
||||
@@ -64,17 +65,15 @@ export class CalendarChannelSyncStatusService {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const calendarChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'calendarChannel',
|
||||
);
|
||||
|
||||
await calendarChannelRepository.update(calendarChannelIds, {
|
||||
syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_ONGOING,
|
||||
syncStatus: CalendarChannelSyncStatus.ONGOING,
|
||||
syncStageStartedAt: new Date().toISOString(),
|
||||
});
|
||||
await this.calendarChannelDataAccessService.update(
|
||||
workspaceId,
|
||||
{ id: In(calendarChannelIds) },
|
||||
{
|
||||
syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_ONGOING,
|
||||
syncStatus: CalendarChannelSyncStatus.ONGOING,
|
||||
syncStageStartedAt: new Date().toISOString(),
|
||||
},
|
||||
);
|
||||
}, authContext);
|
||||
}
|
||||
|
||||
@@ -95,17 +94,15 @@ export class CalendarChannelSyncStatusService {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const calendarChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'calendarChannel',
|
||||
);
|
||||
|
||||
await calendarChannelRepository.update(calendarChannelIds, {
|
||||
syncCursor: '',
|
||||
syncStageStartedAt: null,
|
||||
throttleFailureCount: 0,
|
||||
});
|
||||
await this.calendarChannelDataAccessService.update(
|
||||
workspaceId,
|
||||
{ id: In(calendarChannelIds) },
|
||||
{
|
||||
syncCursor: '',
|
||||
syncStageStartedAt: null,
|
||||
throttleFailureCount: 0,
|
||||
},
|
||||
);
|
||||
}, authContext);
|
||||
|
||||
await this.markAsCalendarEventListFetchPending(
|
||||
@@ -125,15 +122,13 @@ export class CalendarChannelSyncStatusService {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const calendarChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'calendarChannel',
|
||||
);
|
||||
|
||||
await calendarChannelRepository.update(calendarChannelIds, {
|
||||
syncStageStartedAt: null,
|
||||
});
|
||||
await this.calendarChannelDataAccessService.update(
|
||||
workspaceId,
|
||||
{ id: In(calendarChannelIds) },
|
||||
{
|
||||
syncStageStartedAt: null,
|
||||
},
|
||||
);
|
||||
}, authContext);
|
||||
}
|
||||
|
||||
@@ -149,16 +144,14 @@ export class CalendarChannelSyncStatusService {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const calendarChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'calendarChannel',
|
||||
);
|
||||
|
||||
await calendarChannelRepository.update(calendarChannelIds, {
|
||||
syncStage: CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_PENDING,
|
||||
...(!preserveSyncStageStartedAt ? { syncStageStartedAt: null } : {}),
|
||||
});
|
||||
await this.calendarChannelDataAccessService.update(
|
||||
workspaceId,
|
||||
{ id: In(calendarChannelIds) },
|
||||
{
|
||||
syncStage: CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_PENDING,
|
||||
...(!preserveSyncStageStartedAt ? { syncStageStartedAt: null } : {}),
|
||||
},
|
||||
);
|
||||
}, authContext);
|
||||
}
|
||||
|
||||
@@ -173,17 +166,15 @@ export class CalendarChannelSyncStatusService {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const calendarChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'calendarChannel',
|
||||
);
|
||||
|
||||
await calendarChannelRepository.update(calendarChannelIds, {
|
||||
syncStage: CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_ONGOING,
|
||||
syncStatus: CalendarChannelSyncStatus.ONGOING,
|
||||
syncStageStartedAt: new Date().toISOString(),
|
||||
});
|
||||
await this.calendarChannelDataAccessService.update(
|
||||
workspaceId,
|
||||
{ id: In(calendarChannelIds) },
|
||||
{
|
||||
syncStage: CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_ONGOING,
|
||||
syncStatus: CalendarChannelSyncStatus.ONGOING,
|
||||
syncStageStartedAt: new Date().toISOString(),
|
||||
},
|
||||
);
|
||||
}, authContext);
|
||||
}
|
||||
|
||||
@@ -198,19 +189,17 @@ export class CalendarChannelSyncStatusService {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const calendarChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'calendarChannel',
|
||||
);
|
||||
|
||||
await calendarChannelRepository.update(calendarChannelIds, {
|
||||
syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING,
|
||||
syncStatus: CalendarChannelSyncStatus.ACTIVE,
|
||||
throttleFailureCount: 0,
|
||||
syncStageStartedAt: null,
|
||||
syncedAt: new Date().toISOString(),
|
||||
});
|
||||
await this.calendarChannelDataAccessService.update(
|
||||
workspaceId,
|
||||
{ id: In(calendarChannelIds) },
|
||||
{
|
||||
syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING,
|
||||
syncStatus: CalendarChannelSyncStatus.ACTIVE,
|
||||
throttleFailureCount: 0,
|
||||
syncStageStartedAt: null,
|
||||
syncedAt: new Date().toISOString(),
|
||||
},
|
||||
);
|
||||
}, authContext);
|
||||
|
||||
await this.markAsCalendarEventListFetchPending(
|
||||
@@ -241,16 +230,14 @@ export class CalendarChannelSyncStatusService {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const calendarChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'calendarChannel',
|
||||
);
|
||||
|
||||
await calendarChannelRepository.update(calendarChannelIds, {
|
||||
syncStatus: CalendarChannelSyncStatus.FAILED_UNKNOWN,
|
||||
syncStage: CalendarChannelSyncStage.FAILED,
|
||||
});
|
||||
await this.calendarChannelDataAccessService.update(
|
||||
workspaceId,
|
||||
{ id: In(calendarChannelIds) },
|
||||
{
|
||||
syncStatus: CalendarChannelSyncStatus.FAILED_UNKNOWN,
|
||||
syncStage: CalendarChannelSyncStage.FAILED,
|
||||
},
|
||||
);
|
||||
}, authContext);
|
||||
|
||||
await this.metricsService.batchIncrementCounter({
|
||||
@@ -276,33 +263,29 @@ export class CalendarChannelSyncStatusService {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const calendarChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'calendarChannel',
|
||||
);
|
||||
await this.calendarChannelDataAccessService.update(
|
||||
workspaceId,
|
||||
{ id: In(calendarChannelIds) },
|
||||
{
|
||||
syncStatus: CalendarChannelSyncStatus.FAILED_INSUFFICIENT_PERMISSIONS,
|
||||
syncStage: CalendarChannelSyncStage.FAILED,
|
||||
},
|
||||
);
|
||||
|
||||
await calendarChannelRepository.update(calendarChannelIds, {
|
||||
syncStatus: CalendarChannelSyncStatus.FAILED_INSUFFICIENT_PERMISSIONS,
|
||||
syncStage: CalendarChannelSyncStage.FAILED,
|
||||
});
|
||||
|
||||
const connectedAccountRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'connectedAccount',
|
||||
);
|
||||
|
||||
const calendarChannels = await calendarChannelRepository.find({
|
||||
select: ['id', 'connectedAccountId'],
|
||||
where: { id: Any(calendarChannelIds) },
|
||||
});
|
||||
const calendarChannels = await this.calendarChannelDataAccessService.find(
|
||||
workspaceId,
|
||||
{
|
||||
select: ['id', 'connectedAccountId'],
|
||||
where: { id: Any(calendarChannelIds) },
|
||||
},
|
||||
);
|
||||
|
||||
const connectedAccountIds = calendarChannels.map(
|
||||
(calendarChannel) => calendarChannel.connectedAccountId,
|
||||
);
|
||||
|
||||
await connectedAccountRepository.update(
|
||||
await this.connectedAccountDataAccessService.update(
|
||||
workspaceId,
|
||||
{ id: Any(connectedAccountIds) },
|
||||
{
|
||||
authFailedAt: new Date(),
|
||||
@@ -329,26 +312,43 @@ export class CalendarChannelSyncStatusService {
|
||||
return;
|
||||
}
|
||||
|
||||
const calendarChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'calendarChannel',
|
||||
);
|
||||
|
||||
const calendarChannels = await calendarChannelRepository.find({
|
||||
where: {
|
||||
id: Any(calendarChannelIds),
|
||||
},
|
||||
relations: {
|
||||
connectedAccount: {
|
||||
accountOwner: true,
|
||||
const calendarChannels = await this.calendarChannelDataAccessService.find(
|
||||
workspaceId,
|
||||
{
|
||||
select: ['id', 'connectedAccountId'],
|
||||
where: {
|
||||
id: Any(calendarChannelIds),
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
const workspaceMemberRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workspaceMember',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
for (const calendarChannel of calendarChannels) {
|
||||
const userId = calendarChannel.connectedAccount.accountOwner.userId;
|
||||
const connectedAccountId = calendarChannel.connectedAccount.id;
|
||||
const connectedAccount =
|
||||
await this.connectedAccountDataAccessService.findOne(workspaceId, {
|
||||
where: { id: calendarChannel.connectedAccountId },
|
||||
});
|
||||
|
||||
if (!connectedAccount) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const workspaceMember = await workspaceMemberRepository.findOne({
|
||||
where: { id: connectedAccount.accountOwnerId },
|
||||
});
|
||||
|
||||
if (!workspaceMember) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const userId = workspaceMember.userId;
|
||||
const connectedAccountId = connectedAccount.id;
|
||||
|
||||
await this.accountsToReconnectService.addAccountToReconnectByKey(
|
||||
AccountsToReconnectKeys.ACCOUNTS_TO_RECONNECT_INSUFFICIENT_PERMISSIONS,
|
||||
|
||||
+13
-31
@@ -1,6 +1,12 @@
|
||||
import { registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import {
|
||||
CalendarChannelContactAutoCreationPolicy,
|
||||
CalendarChannelSyncStage,
|
||||
CalendarChannelSyncStatus,
|
||||
CalendarChannelVisibility,
|
||||
FieldMetadataType,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { type FieldTypeAndNameMetadata } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
|
||||
@@ -8,36 +14,12 @@ import { type EntityRelation } from 'src/engine/workspace-manager/workspace-migr
|
||||
import { type CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity';
|
||||
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
|
||||
export enum CalendarChannelVisibility {
|
||||
METADATA = 'METADATA',
|
||||
SHARE_EVERYTHING = 'SHARE_EVERYTHING',
|
||||
}
|
||||
|
||||
export enum CalendarChannelSyncStatus {
|
||||
NOT_SYNCED = 'NOT_SYNCED',
|
||||
ONGOING = 'ONGOING',
|
||||
ACTIVE = 'ACTIVE',
|
||||
FAILED_INSUFFICIENT_PERMISSIONS = 'FAILED_INSUFFICIENT_PERMISSIONS',
|
||||
FAILED_UNKNOWN = 'FAILED_UNKNOWN',
|
||||
}
|
||||
|
||||
export enum CalendarChannelSyncStage {
|
||||
PENDING_CONFIGURATION = 'PENDING_CONFIGURATION',
|
||||
CALENDAR_EVENT_LIST_FETCH_PENDING = 'CALENDAR_EVENT_LIST_FETCH_PENDING',
|
||||
CALENDAR_EVENT_LIST_FETCH_SCHEDULED = 'CALENDAR_EVENT_LIST_FETCH_SCHEDULED',
|
||||
CALENDAR_EVENT_LIST_FETCH_ONGOING = 'CALENDAR_EVENT_LIST_FETCH_ONGOING',
|
||||
CALENDAR_EVENTS_IMPORT_PENDING = 'CALENDAR_EVENTS_IMPORT_PENDING',
|
||||
CALENDAR_EVENTS_IMPORT_SCHEDULED = 'CALENDAR_EVENTS_IMPORT_SCHEDULED',
|
||||
CALENDAR_EVENTS_IMPORT_ONGOING = 'CALENDAR_EVENTS_IMPORT_ONGOING',
|
||||
FAILED = 'FAILED',
|
||||
}
|
||||
|
||||
export enum CalendarChannelContactAutoCreationPolicy {
|
||||
AS_PARTICIPANT_AND_ORGANIZER = 'AS_PARTICIPANT_AND_ORGANIZER',
|
||||
AS_PARTICIPANT = 'AS_PARTICIPANT',
|
||||
AS_ORGANIZER = 'AS_ORGANIZER',
|
||||
NONE = 'NONE',
|
||||
}
|
||||
export {
|
||||
CalendarChannelContactAutoCreationPolicy,
|
||||
CalendarChannelSyncStage,
|
||||
CalendarChannelSyncStatus,
|
||||
CalendarChannelVisibility,
|
||||
};
|
||||
|
||||
registerEnumType(CalendarChannelVisibility, {
|
||||
name: 'CalendarChannelVisibility',
|
||||
|
||||
+4
@@ -1,5 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { CalendarChannelDataAccessModule } from 'src/engine/metadata-modules/calendar-channel/data-access/calendar-channel-data-access.module';
|
||||
import { MessageChannelDataAccessModule } from 'src/engine/metadata-modules/message-channel/data-access/message-channel-data-access.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
import { ChannelSyncResolver } from 'src/modules/connected-account/channel-sync/channel-sync.resolver';
|
||||
@@ -8,6 +10,8 @@ import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-co
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
CalendarChannelDataAccessModule,
|
||||
MessageChannelDataAccessModule,
|
||||
PermissionsModule,
|
||||
WorkspaceDataSourceModule,
|
||||
MessagingCommonModule,
|
||||
|
||||
+26
-30
@@ -3,6 +3,8 @@ import { Injectable } from '@nestjs/common';
|
||||
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 { 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 { 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 {
|
||||
@@ -12,13 +14,9 @@ import {
|
||||
import {
|
||||
CalendarChannelSyncStage,
|
||||
CalendarChannelSyncStatus,
|
||||
type CalendarChannelWorkspaceEntity,
|
||||
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service';
|
||||
import {
|
||||
MessageChannelSyncStage,
|
||||
type MessageChannelWorkspaceEntity,
|
||||
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import { MessageChannelSyncStage } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import {
|
||||
MessagingMessageListFetchJob,
|
||||
type MessagingMessageListFetchJobData,
|
||||
@@ -37,7 +35,9 @@ export class ChannelSyncService {
|
||||
private readonly messageQueueService: MessageQueueService,
|
||||
@InjectMessageQueue(MessageQueue.calendarQueue)
|
||||
private readonly calendarQueueService: MessageQueueService,
|
||||
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
|
||||
private readonly messageChannelSyncStatusService: MessageChannelSyncStatusService,
|
||||
private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService,
|
||||
) {}
|
||||
|
||||
async startChannelSync(input: StartChannelSyncInput): Promise<void> {
|
||||
@@ -54,18 +54,13 @@ export class ChannelSyncService {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const messageChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<MessageChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'messageChannel',
|
||||
);
|
||||
|
||||
const messageChannels = await messageChannelRepository.find({
|
||||
where: {
|
||||
const messageChannels = await this.messageChannelDataAccessService.find(
|
||||
workspaceId,
|
||||
{
|
||||
connectedAccountId,
|
||||
syncStage: MessageChannelSyncStage.PENDING_CONFIGURATION,
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
for (const messageChannel of messageChannels) {
|
||||
await this.messageChannelSyncStatusService.markAsMessagesListFetchScheduled(
|
||||
@@ -91,25 +86,26 @@ export class ChannelSyncService {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const calendarChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'calendarChannel',
|
||||
);
|
||||
|
||||
const calendarChannels = await calendarChannelRepository.find({
|
||||
where: {
|
||||
connectedAccountId,
|
||||
syncStage: CalendarChannelSyncStage.PENDING_CONFIGURATION,
|
||||
const calendarChannels = await this.calendarChannelDataAccessService.find(
|
||||
workspaceId,
|
||||
{
|
||||
where: {
|
||||
connectedAccountId,
|
||||
syncStage: CalendarChannelSyncStage.PENDING_CONFIGURATION,
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
for (const calendarChannel of calendarChannels) {
|
||||
await calendarChannelRepository.update(calendarChannel.id, {
|
||||
syncStage:
|
||||
CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED,
|
||||
syncStatus: CalendarChannelSyncStatus.ONGOING,
|
||||
});
|
||||
await this.calendarChannelDataAccessService.update(
|
||||
workspaceId,
|
||||
{ id: calendarChannel.id },
|
||||
{
|
||||
syncStage:
|
||||
CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED,
|
||||
syncStatus: CalendarChannelSyncStatus.ONGOING,
|
||||
},
|
||||
);
|
||||
|
||||
await this.calendarQueueService.add<CalendarEventListFetchJobData>(
|
||||
CalendarEventListFetchJob.name,
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { UserVarsModule } from 'src/engine/core-modules/user/user-vars/user-vars.module';
|
||||
import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module';
|
||||
import { DeleteWorkspaceMemberConnectedAccountsCleanupJob } from 'src/modules/connected-account/jobs/delete-workspace-member-connected-accounts.job';
|
||||
import { ConnectedAccountWorkspaceMemberListener } from 'src/modules/connected-account/listeners/connected-account-workspace-member.listener';
|
||||
import { ConnectedAccountListener } from 'src/modules/connected-account/listeners/connected-account.listener';
|
||||
import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service';
|
||||
|
||||
@Module({
|
||||
imports: [UserVarsModule],
|
||||
imports: [ConnectedAccountDataAccessModule, UserVarsModule],
|
||||
providers: [
|
||||
AccountsToReconnectService,
|
||||
ConnectedAccountListener,
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module';
|
||||
import { GmailEmailAliasErrorHandlerService } from 'src/modules/connected-account/email-alias-manager/drivers/google/services/google-email-alias-error-handler.service';
|
||||
import { GoogleEmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/drivers/google/services/google-email-alias-manager.service';
|
||||
import { MicrosoftEmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/drivers/microsoft/services/microsoft-email-alias-manager.service';
|
||||
@@ -7,7 +8,7 @@ import { EmailAliasManagerService } from 'src/modules/connected-account/email-al
|
||||
import { OAuth2ClientManagerModule } from 'src/modules/connected-account/oauth2-client-manager/oauth2-client-manager.module';
|
||||
|
||||
@Module({
|
||||
imports: [OAuth2ClientManagerModule],
|
||||
imports: [OAuth2ClientManagerModule, ConnectedAccountDataAccessModule],
|
||||
providers: [
|
||||
EmailAliasManagerService,
|
||||
GoogleEmailAliasManagerService,
|
||||
|
||||
+11
-13
@@ -1,8 +1,8 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
||||
import { type Repository } from 'typeorm';
|
||||
|
||||
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 { GoogleEmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/drivers/google/services/google-email-alias-manager.service';
|
||||
import { microsoftGraphMeResponseWithProxyAddresses } from 'src/modules/connected-account/email-alias-manager/drivers/microsoft/mocks/microsoft-api-examples';
|
||||
@@ -15,30 +15,27 @@ import { EmailAliasManagerService } from './email-alias-manager.service';
|
||||
describe('Email Alias Manager Service', () => {
|
||||
let emailAliasManagerService: EmailAliasManagerService;
|
||||
let microsoftEmailAliasManagerService: MicrosoftEmailAliasManagerService;
|
||||
let connectedAccountRepository: Partial<
|
||||
Repository<ConnectedAccountWorkspaceEntity>
|
||||
>;
|
||||
const mockConnectedAccountDataAccessService = {
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
update: jest.fn().mockResolvedValue((arg) => arg),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
connectedAccountRepository = {
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
update: jest.fn().mockResolvedValue((arg) => arg),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: GlobalWorkspaceOrmManager,
|
||||
useValue: {
|
||||
getRepository: jest
|
||||
.fn()
|
||||
.mockResolvedValue(connectedAccountRepository),
|
||||
executeInWorkspaceContext: jest
|
||||
.fn()
|
||||
.mockImplementation((fn: () => any, _authContext?: any) => fn()),
|
||||
},
|
||||
},
|
||||
EmailAliasManagerService,
|
||||
{
|
||||
provide: ConnectedAccountDataAccessService,
|
||||
useValue: mockConnectedAccountDataAccessService,
|
||||
},
|
||||
{
|
||||
provide: GoogleEmailAliasManagerService,
|
||||
useValue: {},
|
||||
@@ -96,7 +93,8 @@ describe('Email Alias Manager Service', () => {
|
||||
microsoftEmailAliasManagerService.getHandleAliases,
|
||||
).toHaveBeenCalledWith(mockConnectedAccount);
|
||||
|
||||
expect(connectedAccountRepository.update).toHaveBeenCalledWith(
|
||||
expect(mockConnectedAccountDataAccessService.update).toHaveBeenCalledWith(
|
||||
'test-workspace-id',
|
||||
{ id: mockConnectedAccount.id },
|
||||
{
|
||||
handleAliases: expectedAliases,
|
||||
|
||||
+4
-7
@@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
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 { GoogleEmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/drivers/google/services/google-email-alias-manager.service';
|
||||
@@ -15,6 +16,7 @@ export class EmailAliasManagerService {
|
||||
private readonly googleEmailAliasManagerService: GoogleEmailAliasManagerService,
|
||||
private readonly microsoftEmailAliasManagerService: MicrosoftEmailAliasManagerService,
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
|
||||
) {}
|
||||
|
||||
public async refreshHandleAliases(
|
||||
@@ -50,13 +52,8 @@ export class EmailAliasManagerService {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const connectedAccountRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'connectedAccount',
|
||||
);
|
||||
|
||||
await connectedAccountRepository.update(
|
||||
await this.connectedAccountDataAccessService.update(
|
||||
workspaceId,
|
||||
{ id: connectedAccount.id },
|
||||
{
|
||||
handleAliases: handleAliases.join(','), // TODO: modify handleAliases to be of fieldmetadatatype array
|
||||
|
||||
@@ -5,6 +5,9 @@ import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { MessageQueueModule } from 'src/engine/core-modules/message-queue/message-queue.module';
|
||||
import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.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 { MessageChannelDataAccessModule } from 'src/engine/metadata-modules/message-channel/data-access/message-channel-data-access.module';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module';
|
||||
@@ -19,6 +22,9 @@ import { ImapSmtpCalDavAPIService } from 'src/modules/connected-account/services
|
||||
TwentyORMModule,
|
||||
FeatureFlagModule,
|
||||
AuthModule,
|
||||
CalendarChannelDataAccessModule,
|
||||
ConnectedAccountDataAccessModule,
|
||||
MessageChannelDataAccessModule,
|
||||
],
|
||||
providers: [ImapSmtpCalDavAPIService],
|
||||
exports: [ImapSmtpCalDavAPIService],
|
||||
|
||||
+3
-8
@@ -1,9 +1,9 @@
|
||||
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
|
||||
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
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 { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
|
||||
export type DeleteWorkspaceMemberConnectedAccountsCleanupJobData = {
|
||||
workspaceId: string;
|
||||
@@ -14,6 +14,7 @@ export type DeleteWorkspaceMemberConnectedAccountsCleanupJobData = {
|
||||
export class DeleteWorkspaceMemberConnectedAccountsCleanupJob {
|
||||
constructor(
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
|
||||
) {}
|
||||
|
||||
@Process(DeleteWorkspaceMemberConnectedAccountsCleanupJob.name)
|
||||
@@ -25,13 +26,7 @@ export class DeleteWorkspaceMemberConnectedAccountsCleanupJob {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const connectedAccountRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'connectedAccount',
|
||||
);
|
||||
|
||||
await connectedAccountRepository.delete({
|
||||
await this.connectedAccountDataAccessService.delete(workspaceId, {
|
||||
accountOwnerId: workspaceMemberId,
|
||||
});
|
||||
}, authContext);
|
||||
|
||||
+3
-9
@@ -10,16 +10,16 @@ import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/wo
|
||||
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
|
||||
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 { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
|
||||
import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
|
||||
@WorkspaceQueryHook(`connectedAccount.destroyOne`)
|
||||
export class ConnectedAccountDeleteOnePreQueryHook
|
||||
implements WorkspacePreQueryHookInstance
|
||||
{
|
||||
constructor(
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
|
||||
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
|
||||
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
) {}
|
||||
@@ -38,13 +38,7 @@ export class ConnectedAccountDeleteOnePreQueryHook
|
||||
const messageChannels =
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
async () => {
|
||||
const messageChannelRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<MessageChannelWorkspaceEntity>(
|
||||
workspace.id,
|
||||
'messageChannel',
|
||||
);
|
||||
|
||||
return messageChannelRepository.findBy({
|
||||
return this.messageChannelDataAccessService.find(workspace.id, {
|
||||
connectedAccountId,
|
||||
});
|
||||
},
|
||||
|
||||
+2
@@ -3,6 +3,7 @@ import { Module } from '@nestjs/common';
|
||||
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
||||
|
||||
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 { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ConnectedAccountDeleteOnePreQueryHook } from 'src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook';
|
||||
|
||||
@@ -10,6 +11,7 @@ import { ConnectedAccountDeleteOnePreQueryHook } from 'src/modules/connected-acc
|
||||
imports: [
|
||||
NestjsQueryTypeOrmModule.forFeature([ObjectMetadataEntity]),
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
MessageChannelDataAccessModule,
|
||||
],
|
||||
providers: [ConnectedAccountDeleteOnePreQueryHook],
|
||||
})
|
||||
|
||||
+2
@@ -1,6 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
|
||||
import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module';
|
||||
import { GoogleAPIRefreshAccessTokenModule } from 'src/modules/connected-account/refresh-tokens-manager/drivers/google/google-api-refresh-access-token.module';
|
||||
import { MicrosoftAPIRefreshAccessTokenModule } from 'src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/microsoft-api-refresh-access-token.module';
|
||||
import { ConnectedAccountRefreshTokensService } from 'src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service';
|
||||
@@ -8,6 +9,7 @@ import { ConnectedAccountRefreshTokensService } from 'src/modules/connected-acco
|
||||
@Module({
|
||||
imports: [
|
||||
JwtModule,
|
||||
ConnectedAccountDataAccessModule,
|
||||
GoogleAPIRefreshAccessTokenModule,
|
||||
MicrosoftAPIRefreshAccessTokenModule,
|
||||
],
|
||||
|
||||
+19
-21
@@ -2,6 +2,7 @@ import { Test, type TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
||||
|
||||
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 { GoogleAPIRefreshAccessTokenService } from 'src/modules/connected-account/refresh-tokens-manager/drivers/google/services/google-api-refresh-tokens.service';
|
||||
import { MicrosoftAPIRefreshAccessTokenService } from 'src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/services/microsoft-api-refresh-tokens.service';
|
||||
@@ -17,7 +18,7 @@ describe('ConnectedAccountRefreshTokensService', () => {
|
||||
let service: ConnectedAccountRefreshTokensService;
|
||||
let googleAPIRefreshAccessTokenService: GoogleAPIRefreshAccessTokenService;
|
||||
let microsoftAPIRefreshAccessTokenService: MicrosoftAPIRefreshAccessTokenService;
|
||||
let globalWorkspaceOrmManager: GlobalWorkspaceOrmManager;
|
||||
let connectedAccountDataAccessService: ConnectedAccountDataAccessService;
|
||||
|
||||
const mockWorkspaceId = 'workspace-123';
|
||||
const mockConnectedAccountId = 'account-456';
|
||||
@@ -44,13 +45,18 @@ describe('ConnectedAccountRefreshTokensService', () => {
|
||||
{
|
||||
provide: GlobalWorkspaceOrmManager,
|
||||
useValue: {
|
||||
getRepository: jest.fn(),
|
||||
executeInWorkspaceContext: jest
|
||||
.fn()
|
||||
|
||||
.mockImplementation((fn: () => any, _authContext?: any) => fn()),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ConnectedAccountDataAccessService,
|
||||
useValue: {
|
||||
update: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@@ -65,9 +71,10 @@ describe('ConnectedAccountRefreshTokensService', () => {
|
||||
module.get<MicrosoftAPIRefreshAccessTokenService>(
|
||||
MicrosoftAPIRefreshAccessTokenService,
|
||||
);
|
||||
globalWorkspaceOrmManager = module.get<GlobalWorkspaceOrmManager>(
|
||||
GlobalWorkspaceOrmManager,
|
||||
);
|
||||
connectedAccountDataAccessService =
|
||||
module.get<ConnectedAccountDataAccessService>(
|
||||
ConnectedAccountDataAccessService,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -96,7 +103,7 @@ describe('ConnectedAccountRefreshTokensService', () => {
|
||||
expect(
|
||||
microsoftAPIRefreshAccessTokenService.refreshTokens,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(globalWorkspaceOrmManager.getRepository).not.toHaveBeenCalled();
|
||||
expect(connectedAccountDataAccessService.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should refresh and save new Microsoft token when expired (lastCredentialsRefreshedAt is old)', async () => {
|
||||
@@ -108,7 +115,6 @@ describe('ConnectedAccountRefreshTokensService', () => {
|
||||
lastCredentialsRefreshedAt: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago
|
||||
} as ConnectedAccountWorkspaceEntity;
|
||||
|
||||
const mockRepository = { update: jest.fn() };
|
||||
const newTokens = {
|
||||
accessToken: mockNewAccessToken,
|
||||
refreshToken: mockRefreshToken,
|
||||
@@ -117,9 +123,6 @@ describe('ConnectedAccountRefreshTokensService', () => {
|
||||
jest
|
||||
.spyOn(microsoftAPIRefreshAccessTokenService, 'refreshTokens')
|
||||
.mockResolvedValue(newTokens);
|
||||
jest
|
||||
.spyOn(globalWorkspaceOrmManager, 'getRepository')
|
||||
.mockResolvedValue(mockRepository as any);
|
||||
|
||||
const result = await service.refreshAndSaveTokens(
|
||||
connectedAccount,
|
||||
@@ -130,7 +133,8 @@ describe('ConnectedAccountRefreshTokensService', () => {
|
||||
expect(
|
||||
microsoftAPIRefreshAccessTokenService.refreshTokens,
|
||||
).toHaveBeenCalledWith(mockRefreshToken);
|
||||
expect(mockRepository.update).toHaveBeenCalledWith(
|
||||
expect(connectedAccountDataAccessService.update).toHaveBeenCalledWith(
|
||||
mockWorkspaceId,
|
||||
{ id: mockConnectedAccountId },
|
||||
expect.objectContaining({
|
||||
...newTokens,
|
||||
@@ -148,7 +152,6 @@ describe('ConnectedAccountRefreshTokensService', () => {
|
||||
lastCredentialsRefreshedAt: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago
|
||||
} as ConnectedAccountWorkspaceEntity;
|
||||
|
||||
const mockRepository = { update: jest.fn() };
|
||||
const newTokens = {
|
||||
accessToken: mockNewAccessToken,
|
||||
refreshToken: mockRefreshToken,
|
||||
@@ -157,9 +160,6 @@ describe('ConnectedAccountRefreshTokensService', () => {
|
||||
jest
|
||||
.spyOn(googleAPIRefreshAccessTokenService, 'refreshTokens')
|
||||
.mockResolvedValue(newTokens);
|
||||
jest
|
||||
.spyOn(globalWorkspaceOrmManager, 'getRepository')
|
||||
.mockResolvedValue(mockRepository as any);
|
||||
|
||||
const result = await service.refreshAndSaveTokens(
|
||||
connectedAccount,
|
||||
@@ -170,7 +170,8 @@ describe('ConnectedAccountRefreshTokensService', () => {
|
||||
expect(
|
||||
googleAPIRefreshAccessTokenService.refreshTokens,
|
||||
).toHaveBeenCalledWith(mockRefreshToken);
|
||||
expect(mockRepository.update).toHaveBeenCalledWith(
|
||||
expect(connectedAccountDataAccessService.update).toHaveBeenCalledWith(
|
||||
mockWorkspaceId,
|
||||
{ id: mockConnectedAccountId },
|
||||
expect.objectContaining({
|
||||
...newTokens,
|
||||
@@ -188,7 +189,6 @@ describe('ConnectedAccountRefreshTokensService', () => {
|
||||
lastCredentialsRefreshedAt: null,
|
||||
} as ConnectedAccountWorkspaceEntity;
|
||||
|
||||
const mockRepository = { update: jest.fn() };
|
||||
const newTokens = {
|
||||
accessToken: mockNewAccessToken,
|
||||
refreshToken: mockRefreshToken,
|
||||
@@ -197,9 +197,6 @@ describe('ConnectedAccountRefreshTokensService', () => {
|
||||
jest
|
||||
.spyOn(microsoftAPIRefreshAccessTokenService, 'refreshTokens')
|
||||
.mockResolvedValue(newTokens);
|
||||
jest
|
||||
.spyOn(globalWorkspaceOrmManager, 'getRepository')
|
||||
.mockResolvedValue(mockRepository as any);
|
||||
|
||||
const result = await service.refreshAndSaveTokens(
|
||||
connectedAccount,
|
||||
@@ -210,7 +207,8 @@ describe('ConnectedAccountRefreshTokensService', () => {
|
||||
expect(
|
||||
microsoftAPIRefreshAccessTokenService.refreshTokens,
|
||||
).toHaveBeenCalledWith(mockRefreshToken);
|
||||
expect(mockRepository.update).toHaveBeenCalledWith(
|
||||
expect(connectedAccountDataAccessService.update).toHaveBeenCalledWith(
|
||||
mockWorkspaceId,
|
||||
{ id: mockConnectedAccountId },
|
||||
expect.objectContaining({
|
||||
...newTokens,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user