Refactor global datasource part 3 (#16447)

## Context
Following https://github.com/twentyhq/twenty/pull/16399
Now using the new global orm manager everywhere and returning a
GlobalDatasource/WorkspaceDatasource based on a feature flag.
This means we now need to wrap all our ORM calls within
executeInWorkspaceContext callback (at least for now) so the global
datasource can dynamically hydrate its context via the new store (the
global datasource does not store anything related to workspaces as it is
now a unique singleton). If feature flag is off it still uses local data
stored in the workspace datasource.
This commit is contained in:
Weiko
2025-12-10 17:17:33 +01:00
committed by GitHub
parent 4f13022774
commit 9bd8f94b3a
203 changed files with 8887 additions and 7237 deletions
@@ -14,7 +14,7 @@ import { type UserEntity } from 'src/engine/core-modules/user/user.entity';
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
type TestingAuthContext = Omit<AuthContext, 'workspace' | 'apiKey' | 'user'> & {
@@ -36,12 +36,16 @@ describe('CreatedByFromAuthContextService', () => {
const mockWorkspaceMemberRepository = {
findOneOrFail: jest.fn(),
};
const twentyORMGlobalManager: jest.Mocked<
Pick<TwentyORMGlobalManager, 'getRepositoryForWorkspace'>
const globalWorkspaceOrmManager: jest.Mocked<
Pick<
GlobalWorkspaceOrmManager,
'getRepository' | 'executeInWorkspaceContext'
>
> = {
getRepositoryForWorkspace: jest
getRepository: jest.fn().mockResolvedValue(mockWorkspaceMemberRepository),
executeInWorkspaceContext: jest
.fn()
.mockResolvedValue(mockWorkspaceMemberRepository),
.mockImplementation((_authContext: any, fn: () => any) => fn()),
};
beforeEach(async () => {
@@ -49,8 +53,8 @@ describe('CreatedByFromAuthContextService', () => {
providers: [
CreatedByFromAuthContextService,
{
provide: TwentyORMGlobalManager,
useValue: twentyORMGlobalManager,
provide: GlobalWorkspaceOrmManager,
useValue: globalWorkspaceOrmManager,
},
{
provide: getRepositoryToken(FieldMetadataEntity),
@@ -3,6 +3,8 @@ import { Injectable, Logger } from '@nestjs/common';
import { type ActorMetadata } from 'twenty-shared/types';
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { buildCreatedByFromApiKey } from 'src/engine/core-modules/actor/utils/build-created-by-from-api-key.util';
import { buildCreatedByFromFullNameMetadata } from 'src/engine/core-modules/actor/utils/build-created-by-from-full-name-metadata.util';
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
@@ -10,7 +12,7 @@ import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util';
import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -21,7 +23,7 @@ export class CreatedByFromAuthContextService {
private readonly logger = new Logger(CreatedByFromAuthContextService.name);
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {}
@@ -109,23 +111,30 @@ export class CreatedByFromAuthContextService {
assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError);
if (isDefined(user)) {
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext as WorkspaceAuthContext,
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const workspaceMember = await workspaceMemberRepository.findOneOrFail({
where: {
userId: user.id,
const workspaceMember = await workspaceMemberRepository.findOneOrFail(
{
where: {
userId: user.id,
},
},
);
return buildCreatedByFromFullNameMetadata({
fullNameMetadata: workspaceMember.name,
workspaceMemberId: workspaceMember.id,
});
},
});
return buildCreatedByFromFullNameMetadata({
fullNameMetadata: workspaceMember.name,
workspaceMemberId: workspaceMember.id,
});
);
}
if (isDefined(apiKey)) {
@@ -17,7 +17,8 @@ import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.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 { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@UseGuards(
@@ -32,7 +33,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
@Resolver()
export class ApprovedAccessDomainResolver {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly approvedAccessDomainService: ApprovedAccessDomainService,
) {}
@@ -42,18 +43,26 @@ export class ApprovedAccessDomainResolver {
@AuthWorkspace() currentWorkspace: WorkspaceEntity,
@AuthUser() currentUser: UserEntity,
): Promise<ApprovedAccessDomainDTO> {
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
currentWorkspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(currentWorkspace.id);
const workspaceMember = await workspaceMemberRepository.findOneOrFail({
where: {
userId: currentUser.id,
},
});
const workspaceMember =
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
currentWorkspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
return workspaceMemberRepository.findOneOrFail({
where: {
userId: currentUser.id,
},
});
},
);
return this.approvedAccessDomainService.createApprovedAccessDomain(
domain,
@@ -3,7 +3,8 @@ import { Injectable } from '@nestjs/common';
import { v4 } from 'uuid';
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
import {
CalendarChannelSyncStage,
CalendarChannelSyncStatus,
@@ -22,7 +23,7 @@ export type CreateCalendarChannelInput = {
@Injectable()
export class CreateCalendarChannelService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
) {}
async createCalendarChannel(
@@ -36,26 +37,33 @@ export class CreateCalendarChannelService {
manager,
} = input;
const calendarChannelRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<CalendarChannelWorkspaceEntity>(
workspaceId,
'calendarChannel',
);
const authContext = buildSystemAuthContext(workspaceId);
const newCalendarChannel = await calendarChannelRepository.save(
{
id: v4(),
connectedAccountId,
handle,
visibility:
calendarVisibility || CalendarChannelVisibility.SHARE_EVERYTHING,
syncStatus: CalendarChannelSyncStatus.NOT_SYNCED,
syncStage: CalendarChannelSyncStage.PENDING_CONFIGURATION,
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const calendarChannelRepository =
await this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
workspaceId,
'calendarChannel',
);
const newCalendarChannel = await calendarChannelRepository.save(
{
id: v4(),
connectedAccountId,
handle,
visibility:
calendarVisibility || CalendarChannelVisibility.SHARE_EVERYTHING,
syncStatus: CalendarChannelSyncStatus.NOT_SYNCED,
syncStage: CalendarChannelSyncStage.PENDING_CONFIGURATION,
},
{},
manager,
);
return newCalendarChannel.id;
},
{},
manager,
);
return newCalendarChannel.id;
}
}
@@ -3,7 +3,8 @@ import { Injectable } from '@nestjs/common';
import { type ConnectedAccountProvider } from 'twenty-shared/types';
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.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 = {
@@ -21,7 +22,7 @@ export type CreateConnectedAccountInput = {
@Injectable()
export class CreateConnectedAccountService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
) {}
async createConnectedAccount(
@@ -39,24 +40,31 @@ export class CreateConnectedAccountService {
manager,
} = input;
const connectedAccountRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const authContext = buildSystemAuthContext(workspaceId);
await connectedAccountRepository.save(
{
id: connectedAccountId,
handle,
provider,
accessToken,
refreshToken,
accountOwnerId,
scopes,
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const connectedAccountRepository =
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
await connectedAccountRepository.save(
{
id: connectedAccountId,
handle,
provider,
accessToken,
refreshToken,
accountOwnerId,
scopes,
},
{},
manager,
);
},
{},
manager,
);
}
}
@@ -4,7 +4,8 @@ import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.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';
import {
MessageChannelPendingGroupEmailsAction,
@@ -27,7 +28,7 @@ export type CreateMessageChannelInput = {
@Injectable()
export class CreateMessageChannelService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly syncMessageFoldersService: SyncMessageFoldersService,
) {}
@@ -42,49 +43,57 @@ export class CreateMessageChannelService {
manager,
} = input;
const connectedAccountRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const authContext = buildSystemAuthContext(workspaceId);
const connectedAccount = await connectedAccountRepository.findOne({
where: { id: connectedAccountId },
});
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const connectedAccountRepository =
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const messageChannelRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<MessageChannelWorkspaceEntity>(
workspaceId,
'messageChannel',
);
const connectedAccount = await connectedAccountRepository.findOne({
where: { id: connectedAccountId },
});
const newMessageChannel = await messageChannelRepository.save(
{
id: v4(),
connectedAccountId,
type: MessageChannelType.EMAIL,
handle,
visibility:
messageVisibility || MessageChannelVisibility.SHARE_EVERYTHING,
syncStatus: MessageChannelSyncStatus.NOT_SYNCED,
syncStage: MessageChannelSyncStage.PENDING_CONFIGURATION,
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction.NONE,
const messageChannelRepository =
await this.globalWorkspaceOrmManager.getRepository<MessageChannelWorkspaceEntity>(
workspaceId,
'messageChannel',
);
const newMessageChannel = await messageChannelRepository.save(
{
id: v4(),
connectedAccountId,
type: MessageChannelType.EMAIL,
handle,
visibility:
messageVisibility || MessageChannelVisibility.SHARE_EVERYTHING,
syncStatus: MessageChannelSyncStatus.NOT_SYNCED,
syncStage: MessageChannelSyncStage.PENDING_CONFIGURATION,
pendingGroupEmailsAction:
MessageChannelPendingGroupEmailsAction.NONE,
},
{},
manager,
);
if (isDefined(connectedAccount)) {
await this.syncMessageFoldersService.syncMessageFolders({
workspaceId,
messageChannel: {
...newMessageChannel,
connectedAccount,
},
manager,
});
}
return newMessageChannel.id;
},
{},
manager,
);
if (isDefined(connectedAccount)) {
await this.syncMessageFoldersService.syncMessageFolders({
workspaceId,
messageChannel: {
...newMessageChannel,
connectedAccount,
},
manager,
});
}
return newMessageChannel.id;
}
}
@@ -13,7 +13,7 @@ import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queu
import { getQueueToken } from 'src/engine/core-modules/message-queue/utils/get-queue-token.util';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service';
import {
CalendarChannelSyncStage,
@@ -77,9 +77,9 @@ describe('GoogleAPIsService', () => {
providers: [
GoogleAPIsService,
{
provide: TwentyORMGlobalManager,
provide: GlobalWorkspaceOrmManager,
useValue: {
getRepositoryForWorkspace: jest
getRepository: jest
.fn()
.mockImplementation((_workspaceId, entity) => {
if (entity === 'connectedAccount')
@@ -96,6 +96,9 @@ describe('GoogleAPIsService', () => {
getDataSourceForWorkspace: jest
.fn()
.mockImplementation(() => mockWorkspaceDataSource),
executeInWorkspaceContext: jest
.fn()
.mockImplementation((_authContext: any, fn: () => any) => fn()),
},
},
{
@@ -17,7 +17,8 @@ import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queu
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 { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.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 {
CalendarEventListFetchJob,
type CalendarEventListFetchJobData,
@@ -45,7 +46,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
@Injectable()
export class GoogleAPIsService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
@InjectMessageQueue(MessageQueue.messagingQueue)
private readonly messageQueueService: MessageQueueService,
@InjectMessageQueue(MessageQueue.calendarQueue)
@@ -82,36 +83,6 @@ export class GoogleAPIsService {
'CALENDAR_PROVIDER_GOOGLE_ENABLED',
);
const connectedAccountRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const connectedAccount = await connectedAccountRepository.findOne({
where: { handle, accountOwnerId: workspaceMemberId },
});
const existingAccountId = connectedAccount?.id;
const newOrExistingConnectedAccountId = existingAccountId ?? v4();
const calendarChannelRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<CalendarChannelWorkspaceEntity>(
workspaceId,
'calendarChannel',
);
const messageChannelRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<MessageChannelWorkspaceEntity>(
workspaceId,
'messageChannel',
);
const workspaceDataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
workspaceId,
});
const { scopes, isValid } =
await this.googleAPIScopesService.getScopesFromGoogleAccessTokenAndCheckIfExpectedScopesArePresent(
input.accessToken,
@@ -124,131 +95,167 @@ export class GoogleAPIsService {
);
}
await workspaceDataSource.transaction(
async (manager: WorkspaceEntityManager) => {
if (!existingAccountId) {
await this.createConnectedAccountService.createConnectedAccount({
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
handle,
provider: ConnectedAccountProvider.GOOGLE,
accessToken: input.accessToken,
refreshToken: input.refreshToken,
accountOwnerId: workspaceMemberId,
scopes,
manager,
});
const authContext = buildSystemAuthContext(workspaceId);
await this.createMessageChannelService.createMessageChannel({
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const connectedAccountRepository =
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
handle,
messageVisibility,
manager,
});
'connectedAccount',
);
if (isCalendarEnabled) {
await this.createCalendarChannelService.createCalendarChannel({
workspaceId,
const connectedAccount = await connectedAccountRepository.findOne({
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.getDataSourceForWorkspace(
workspaceId,
);
await workspaceDataSource.transaction(
async (manager: WorkspaceEntityManager) => {
if (!existingAccountId) {
await this.createConnectedAccountService.createConnectedAccount({
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
handle,
provider: ConnectedAccountProvider.GOOGLE,
accessToken: input.accessToken,
refreshToken: input.refreshToken,
accountOwnerId: workspaceMemberId,
scopes,
manager,
});
await this.createMessageChannelService.createMessageChannel({
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
handle,
messageVisibility,
manager,
});
if (isCalendarEnabled) {
await this.createCalendarChannelService.createCalendarChannel({
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
handle,
calendarVisibility,
manager,
});
}
} else {
await this.updateConnectedAccountOnReconnectService.updateConnectedAccountOnReconnect(
{
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
accessToken: input.accessToken,
refreshToken: input.refreshToken,
scopes,
connectedAccount,
manager,
},
);
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const workspaceMember =
await workspaceMemberRepository.findOneOrFail({
where: { id: workspaceMemberId },
});
const userId = workspaceMember.userId;
await this.accountsToReconnectService.removeAccountToReconnect(
userId,
workspaceId,
newOrExistingConnectedAccountId,
);
await this.messagingChannelSyncStatusService.resetAndMarkAsMessagesListFetchPending(
[newOrExistingConnectedAccountId],
workspaceId,
);
await this.calendarChannelSyncStatusService.resetAndMarkAsCalendarEventListFetchPending(
[newOrExistingConnectedAccountId],
workspaceId,
);
}
},
);
if (this.twentyConfigService.get('MESSAGING_PROVIDER_GMAIL_ENABLED')) {
const messageChannels = await messageChannelRepository.find({
where: {
connectedAccountId: newOrExistingConnectedAccountId,
handle,
calendarVisibility,
manager,
});
},
});
for (const messageChannel of messageChannels) {
if (
messageChannel.syncStage !==
MessageChannelSyncStage.PENDING_CONFIGURATION
) {
await this.messageQueueService.add<MessagingMessageListFetchJobData>(
MessagingMessageListFetchJob.name,
{
workspaceId,
messageChannelId: messageChannel.id,
},
);
}
}
} else {
await this.updateConnectedAccountOnReconnectService.updateConnectedAccountOnReconnect(
{
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
accessToken: input.accessToken,
refreshToken: input.refreshToken,
scopes,
connectedAccount,
manager,
},
);
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const workspaceMember = await workspaceMemberRepository.findOneOrFail(
{
where: { id: workspaceMemberId },
},
);
const userId = workspaceMember.userId;
await this.accountsToReconnectService.removeAccountToReconnect(
userId,
workspaceId,
newOrExistingConnectedAccountId,
);
await this.messagingChannelSyncStatusService.resetAndMarkAsMessagesListFetchPending(
[newOrExistingConnectedAccountId],
workspaceId,
);
await this.calendarChannelSyncStatusService.resetAndMarkAsCalendarEventListFetchPending(
[newOrExistingConnectedAccountId],
workspaceId,
);
}
if (isCalendarEnabled) {
const calendarChannels = await calendarChannelRepository.find({
where: {
connectedAccountId: newOrExistingConnectedAccountId,
},
});
for (const calendarChannel of calendarChannels) {
if (
calendarChannel.syncStage !==
CalendarChannelSyncStage.PENDING_CONFIGURATION
) {
await this.calendarQueueService.add<CalendarEventListFetchJobData>(
CalendarEventListFetchJob.name,
{
calendarChannelId: calendarChannel.id,
workspaceId,
},
);
}
}
}
return newOrExistingConnectedAccountId;
},
);
if (this.twentyConfigService.get('MESSAGING_PROVIDER_GMAIL_ENABLED')) {
const messageChannels = await messageChannelRepository.find({
where: {
connectedAccountId: newOrExistingConnectedAccountId,
},
});
for (const messageChannel of messageChannels) {
if (
messageChannel.syncStage !==
MessageChannelSyncStage.PENDING_CONFIGURATION
) {
await this.messageQueueService.add<MessagingMessageListFetchJobData>(
MessagingMessageListFetchJob.name,
{
workspaceId,
messageChannelId: messageChannel.id,
},
);
}
}
}
if (isCalendarEnabled) {
const calendarChannels = await calendarChannelRepository.find({
where: {
connectedAccountId: newOrExistingConnectedAccountId,
},
});
for (const calendarChannel of calendarChannels) {
if (
calendarChannel.syncStage !==
CalendarChannelSyncStage.PENDING_CONFIGURATION
) {
await this.calendarQueueService.add<CalendarEventListFetchJobData>(
CalendarEventListFetchJob.name,
{
calendarChannelId: calendarChannel.id,
workspaceId,
},
);
}
}
}
return newOrExistingConnectedAccountId;
}
}
@@ -12,7 +12,7 @@ import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queu
import { getQueueToken } from 'src/engine/core-modules/message-queue/utils/get-queue-token.util';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service';
import {
CalendarChannelSyncStage,
@@ -76,9 +76,9 @@ describe('MicrosoftAPIsService', () => {
providers: [
MicrosoftAPIsService,
{
provide: TwentyORMGlobalManager,
provide: GlobalWorkspaceOrmManager,
useValue: {
getRepositoryForWorkspace: jest
getRepository: jest
.fn()
.mockImplementation((_workspaceId, entity) => {
if (entity === 'connectedAccount')
@@ -95,6 +95,9 @@ describe('MicrosoftAPIsService', () => {
getDataSourceForWorkspace: jest
.fn()
.mockImplementation(() => mockWorkspaceDataSource),
executeInWorkspaceContext: jest
.fn()
.mockImplementation((_authContext: any, fn: () => any) => fn()),
},
},
{
@@ -13,7 +13,8 @@ import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queu
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 { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.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 {
CalendarEventListFetchJob,
type CalendarEventListFetchJobData,
@@ -41,7 +42,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
@Injectable()
export class MicrosoftAPIsService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
@InjectMessageQueue(MessageQueue.messagingQueue)
private readonly messageQueueService: MessageQueueService,
@InjectMessageQueue(MessageQueue.calendarQueue)
@@ -73,170 +74,177 @@ export class MicrosoftAPIsService {
messageVisibility,
} = input;
const connectedAccountRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const connectedAccount = await connectedAccountRepository.findOne({
where: { handle, accountOwnerId: workspaceMemberId },
});
const existingAccountId = connectedAccount?.id;
const newOrExistingConnectedAccountId = existingAccountId ?? v4();
const calendarChannelRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<CalendarChannelWorkspaceEntity>(
workspaceId,
'calendarChannel',
);
const messageChannelRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<MessageChannelWorkspaceEntity>(
workspaceId,
'messageChannel',
);
const workspaceDataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
workspaceId,
});
const scopes = getMicrosoftApisOauthScopes();
await workspaceDataSource.transaction(
async (manager: WorkspaceEntityManager) => {
if (!existingAccountId) {
await this.createConnectedAccountService.createConnectedAccount({
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
handle,
provider: ConnectedAccountProvider.MICROSOFT,
accessToken: input.accessToken,
refreshToken: input.refreshToken,
accountOwnerId: workspaceMemberId,
scopes,
manager,
});
const authContext = buildSystemAuthContext(workspaceId);
await this.createMessageChannelService.createMessageChannel({
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const connectedAccountRepository =
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
handle,
messageVisibility,
manager,
});
'connectedAccount',
);
if (
this.twentyConfigService.get('CALENDAR_PROVIDER_MICROSOFT_ENABLED')
) {
await this.createCalendarChannelService.createCalendarChannel({
workspaceId,
const connectedAccount = await connectedAccountRepository.findOne({
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.getDataSourceForWorkspace(
workspaceId,
);
await workspaceDataSource.transaction(
async (manager: WorkspaceEntityManager) => {
if (!existingAccountId) {
await this.createConnectedAccountService.createConnectedAccount({
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
handle,
provider: ConnectedAccountProvider.MICROSOFT,
accessToken: input.accessToken,
refreshToken: input.refreshToken,
accountOwnerId: workspaceMemberId,
scopes,
manager,
});
await this.createMessageChannelService.createMessageChannel({
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
handle,
messageVisibility,
manager,
});
if (
this.twentyConfigService.get(
'CALENDAR_PROVIDER_MICROSOFT_ENABLED',
)
) {
await this.createCalendarChannelService.createCalendarChannel({
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
handle,
calendarVisibility,
manager,
});
}
} else {
await this.updateConnectedAccountOnReconnectService.updateConnectedAccountOnReconnect(
{
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
accessToken: input.accessToken,
refreshToken: input.refreshToken,
scopes,
connectedAccount,
manager,
},
);
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const workspaceMember =
await workspaceMemberRepository.findOneOrFail({
where: { id: workspaceMemberId },
});
const userId = workspaceMember.userId;
await this.accountsToReconnectService.removeAccountToReconnect(
userId,
workspaceId,
newOrExistingConnectedAccountId,
);
await this.messagingChannelSyncStatusService.resetAndMarkAsMessagesListFetchPending(
[newOrExistingConnectedAccountId],
workspaceId,
);
await this.calendarChannelSyncStatusService.resetAndMarkAsCalendarEventListFetchPending(
[newOrExistingConnectedAccountId],
workspaceId,
);
}
},
);
if (
this.twentyConfigService.get('MESSAGING_PROVIDER_MICROSOFT_ENABLED')
) {
const messageChannels = await messageChannelRepository.find({
where: {
connectedAccountId: newOrExistingConnectedAccountId,
handle,
calendarVisibility,
manager,
});
},
});
for (const messageChannel of messageChannels) {
if (
messageChannel.syncStage !==
MessageChannelSyncStage.PENDING_CONFIGURATION
) {
await this.messageQueueService.add<MessagingMessageListFetchJobData>(
MessagingMessageListFetchJob.name,
{
workspaceId,
messageChannelId: messageChannel.id,
},
);
}
}
} else {
await this.updateConnectedAccountOnReconnectService.updateConnectedAccountOnReconnect(
{
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
accessToken: input.accessToken,
refreshToken: input.refreshToken,
scopes,
connectedAccount,
manager,
},
);
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const workspaceMember = await workspaceMemberRepository.findOneOrFail(
{
where: { id: workspaceMemberId },
},
);
const userId = workspaceMember.userId;
await this.accountsToReconnectService.removeAccountToReconnect(
userId,
workspaceId,
newOrExistingConnectedAccountId,
);
await this.messagingChannelSyncStatusService.resetAndMarkAsMessagesListFetchPending(
[newOrExistingConnectedAccountId],
workspaceId,
);
await this.calendarChannelSyncStatusService.resetAndMarkAsCalendarEventListFetchPending(
[newOrExistingConnectedAccountId],
workspaceId,
);
await this.calendarChannelSyncStatusService.resetAndMarkAsCalendarEventListFetchPending(
[newOrExistingConnectedAccountId],
workspaceId,
);
}
if (
this.twentyConfigService.get('CALENDAR_PROVIDER_MICROSOFT_ENABLED')
) {
const calendarChannels = await calendarChannelRepository.find({
where: {
connectedAccountId: newOrExistingConnectedAccountId,
},
});
for (const calendarChannel of calendarChannels) {
if (
calendarChannel.syncStage !==
CalendarChannelSyncStage.PENDING_CONFIGURATION
) {
await this.calendarQueueService.add<CalendarEventListFetchJobData>(
CalendarEventListFetchJob.name,
{
calendarChannelId: calendarChannel.id,
workspaceId,
},
);
}
}
}
return newOrExistingConnectedAccountId;
},
);
if (this.twentyConfigService.get('MESSAGING_PROVIDER_MICROSOFT_ENABLED')) {
const messageChannels = await messageChannelRepository.find({
where: {
connectedAccountId: newOrExistingConnectedAccountId,
},
});
for (const messageChannel of messageChannels) {
if (
messageChannel.syncStage !==
MessageChannelSyncStage.PENDING_CONFIGURATION
) {
await this.messageQueueService.add<MessagingMessageListFetchJobData>(
MessagingMessageListFetchJob.name,
{
workspaceId,
messageChannelId: messageChannel.id,
},
);
}
}
}
if (this.twentyConfigService.get('CALENDAR_PROVIDER_MICROSOFT_ENABLED')) {
const calendarChannels = await calendarChannelRepository.find({
where: {
connectedAccountId: newOrExistingConnectedAccountId,
},
});
for (const calendarChannel of calendarChannels) {
if (
calendarChannel.syncStage !==
CalendarChannelSyncStage.PENDING_CONFIGURATION
) {
await this.calendarQueueService.add<CalendarEventListFetchJobData>(
CalendarEventListFetchJob.name,
{
calendarChannelId: calendarChannel.id,
workspaceId,
},
);
}
}
}
return newOrExistingConnectedAccountId;
}
}
@@ -1,7 +1,8 @@
import { Injectable } from '@nestjs/common';
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.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 = {
@@ -17,7 +18,7 @@ export type UpdateConnectedAccountOnReconnectInput = {
@Injectable()
export class UpdateConnectedAccountOnReconnectService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
) {}
async updateConnectedAccountOnReconnect(
@@ -32,23 +33,30 @@ export class UpdateConnectedAccountOnReconnectService {
manager,
} = input;
const connectedAccountRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const authContext = buildSystemAuthContext(workspaceId);
await connectedAccountRepository.update(
{
id: connectedAccountId,
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const connectedAccountRepository =
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
await connectedAccountRepository.update(
{
id: connectedAccountId,
},
{
accessToken,
refreshToken,
scopes,
authFailedAt: null,
},
manager,
);
},
{
accessToken,
refreshToken,
scopes,
authFailedAt: null,
},
manager,
);
}
}
@@ -17,7 +17,7 @@ import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { AccessTokenService } from './access-token.service';
@@ -27,7 +27,7 @@ describe('AccessTokenService', () => {
let twentyConfigService: TwentyConfigService;
let userRepository: Repository<UserEntity>;
let workspaceRepository: Repository<WorkspaceEntity>;
let twentyORMGlobalManager: TwentyORMGlobalManager;
let globalWorkspaceOrmManager: GlobalWorkspaceOrmManager;
let userWorkspaceRepository: Repository<UserWorkspaceEntity>;
beforeEach(async () => {
@@ -77,9 +77,12 @@ describe('AccessTokenService', () => {
useValue: {},
},
{
provide: TwentyORMGlobalManager,
provide: GlobalWorkspaceOrmManager,
useValue: {
getRepositoryForWorkspace: jest.fn(),
getRepository: jest.fn(),
executeInWorkspaceContext: jest
.fn()
.mockImplementation((_authContext: any, fn: () => any) => fn()),
},
},
],
@@ -94,8 +97,8 @@ describe('AccessTokenService', () => {
workspaceRepository = module.get<Repository<WorkspaceEntity>>(
getRepositoryToken(WorkspaceEntity),
);
twentyORMGlobalManager = module.get<TwentyORMGlobalManager>(
TwentyORMGlobalManager,
globalWorkspaceOrmManager = module.get<GlobalWorkspaceOrmManager>(
GlobalWorkspaceOrmManager,
);
userWorkspaceRepository = module.get<Repository<UserWorkspaceEntity>>(
getRepositoryToken(UserWorkspaceEntity),
@@ -131,11 +134,9 @@ describe('AccessTokenService', () => {
jest
.spyOn(userWorkspaceRepository, 'findOne')
.mockResolvedValue(mockUserWorkspace as UserWorkspaceEntity);
jest
.spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace')
.mockResolvedValue({
findOne: jest.fn().mockResolvedValue(mockWorkspaceMember),
} as any);
jest.spyOn(globalWorkspaceOrmManager, 'getRepository').mockResolvedValue({
findOne: jest.fn().mockResolvedValue(mockWorkspaceMember),
} as any);
jest.spyOn(jwtWrapperService, 'sign').mockReturnValue(mockToken);
const result = await service.generateAccessToken({
@@ -192,11 +193,9 @@ describe('AccessTokenService', () => {
id: impersonatedUserWorkspaceId,
workspaceId,
} as UserWorkspaceEntity);
jest
.spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace')
.mockResolvedValue({
findOne: jest.fn().mockResolvedValue(mockWorkspaceMember),
} as any);
jest.spyOn(globalWorkspaceOrmManager, 'getRepository').mockResolvedValue({
findOne: jest.fn().mockResolvedValue(mockWorkspaceMember),
} as any);
const signSpy = jest
.spyOn(jwtWrapperService, 'sign')
.mockReturnValue(mockToken);
@@ -28,7 +28,8 @@ import { UserEntity } from 'src/engine/core-modules/user/user.entity';
import { userValidator } from 'src/engine/core-modules/user/user.validate';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.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 { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Injectable()
@@ -41,7 +42,7 @@ export class AccessTokenService {
private readonly userRepository: Repository<UserEntity>,
@InjectRepository(WorkspaceEntity)
private readonly workspaceRepository: Repository<WorkspaceEntity>,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
@InjectRepository(UserWorkspaceEntity)
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
) {}
@@ -79,31 +80,39 @@ export class AccessTokenService {
assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError);
if (isWorkspaceActiveOrSuspended(workspace)) {
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspaceId);
const workspaceMember = await workspaceMemberRepository.findOne({
where: {
userId: user.id,
},
});
tokenWorkspaceMemberId =
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
assertIsDefinedOrThrow(
workspaceMember,
new AuthException(
'User is not a member of the workspace',
AuthExceptionCode.FORBIDDEN_EXCEPTION,
{
userFriendlyMessage: msg`User is not a member of the workspace.`,
const workspaceMember = await workspaceMemberRepository.findOne({
where: {
userId: user.id,
},
});
assertIsDefinedOrThrow(
workspaceMember,
new AuthException(
'User is not a member of the workspace',
AuthExceptionCode.FORBIDDEN_EXCEPTION,
{
userFriendlyMessage: msg`User is not a member of the workspace.`,
},
),
);
return workspaceMember.id;
},
),
);
tokenWorkspaceMemberId = workspaceMember.id;
);
}
const userWorkspace = await this.userWorkspaceRepository.findOne({
where: {
@@ -12,7 +12,7 @@ import { BillingCustomerEntity } from 'src/engine/core-modules/billing/entities/
import { StripeSubscriptionService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
@Command({
name: 'billing:sync-customer-data',
@@ -25,10 +25,10 @@ export class BillingSyncCustomerDataCommand extends ActiveOrSuspendedWorkspacesM
private readonly stripeSubscriptionService: StripeSubscriptionService,
@InjectRepository(BillingCustomerEntity)
protected readonly billingCustomerRepository: Repository<BillingCustomerEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService);
}
override async runOnWorkspace({
@@ -13,7 +13,7 @@ import { BillingSubscriptionService } from 'src/engine/core-modules/billing/serv
import { StripeSubscriptionItemService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription-item.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
@Command({
name: 'billing:update-subscription-price',
@@ -27,14 +27,14 @@ export class BillingUpdateSubscriptionPriceCommand extends ActiveOrSuspendedWork
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
@InjectRepository(BillingSubscriptionEntity)
protected readonly billingSubscriptionRepository: Repository<BillingSubscriptionEntity>,
private readonly billingSubscriptionService: BillingSubscriptionService,
private readonly stripeSubscriptionItemService: StripeSubscriptionItemService,
protected readonly dataSourceService: DataSourceService,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService);
}
@Option({
@@ -7,7 +7,8 @@ import { StripeSubscriptionItemService } from 'src/engine/core-modules/billing/s
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 { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.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 { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
export type UpdateSubscriptionQuantityJobData = { workspaceId: string };
@@ -21,41 +22,48 @@ export class UpdateSubscriptionQuantityJob {
constructor(
private readonly billingSubscriptionService: BillingSubscriptionService,
private readonly stripeSubscriptionItemService: StripeSubscriptionItemService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
) {}
@Process(UpdateSubscriptionQuantityJob.name)
async handle(data: UpdateSubscriptionQuantityJobData): Promise<void> {
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
data.workspaceId,
'workspaceMember',
);
const authContext = buildSystemAuthContext(data.workspaceId);
const workspaceMembersCount = await workspaceMemberRepository.count();
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
data.workspaceId,
'workspaceMember',
);
if (!workspaceMembersCount || workspaceMembersCount <= 0) {
return;
}
const workspaceMembersCount = await workspaceMemberRepository.count();
try {
const billingBaseProductSubscriptionItem =
await this.billingSubscriptionService.getBaseProductCurrentBillingSubscriptionItemOrThrow(
data.workspaceId,
);
if (!workspaceMembersCount || workspaceMembersCount <= 0) {
return;
}
await this.stripeSubscriptionItemService.updateSubscriptionItem(
billingBaseProductSubscriptionItem.stripeSubscriptionItemId,
{ quantity: workspaceMembersCount },
);
try {
const billingBaseProductSubscriptionItem =
await this.billingSubscriptionService.getBaseProductCurrentBillingSubscriptionItemOrThrow(
data.workspaceId,
);
this.logger.log(
`Updating workspace ${data.workspaceId} subscription quantity to ${workspaceMembersCount} members`,
);
} catch (e) {
this.logger.warn(
`Failed to update workspace ${data.workspaceId} subscription quantity to ${workspaceMembersCount} members. Error: ${e}`,
);
}
await this.stripeSubscriptionItemService.updateSubscriptionItem(
billingBaseProductSubscriptionItem.stripeSubscriptionItemId,
{ quantity: workspaceMembersCount },
);
this.logger.log(
`Updating workspace ${data.workspaceId} subscription quantity to ${workspaceMembersCount} members`,
);
} catch (e) {
this.logger.warn(
`Failed to update workspace ${data.workspaceId} subscription quantity to ${workspaceMembersCount} members. Error: ${e}`,
);
}
},
);
}
}
@@ -2,8 +2,8 @@ import { Test, type TestingModule } from '@nestjs/testing';
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.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';
@@ -36,18 +36,19 @@ describe('TimelineCalendarEventService', () => {
findAndCount: jest.fn(),
};
const mockTwentyORMGlobalManager = {
getRepositoryForWorkspace: jest
const mockGlobalWorkspaceOrmManager = {
getRepository: jest.fn().mockResolvedValue(mockCalendarEventRepository),
executeInWorkspaceContext: jest
.fn()
.mockResolvedValue(mockCalendarEventRepository),
.mockImplementation((_authContext: any, fn: () => any) => fn()),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
TimelineCalendarEventService,
{
provide: TwentyORMGlobalManager,
useValue: mockTwentyORMGlobalManager,
provide: GlobalWorkspaceOrmManager,
useValue: mockGlobalWorkspaceOrmManager,
},
],
}).compile();
@@ -6,7 +6,8 @@ import { Any } from 'typeorm';
import { TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE } from 'src/engine/core-modules/calendar/constants/calendar.constants';
import { type TimelineCalendarEventsWithTotalDTO } from 'src/engine/core-modules/calendar/dtos/timeline-calendar-events-with-total.dto';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.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 { CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { type CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity';
import { type OpportunityWorkspaceEntity } from 'src/modules/opportunity/standard-objects/opportunity.workspace-entity';
@@ -15,10 +16,9 @@ import { type PersonWorkspaceEntity } from 'src/modules/person/standard-objects/
@Injectable()
export class TimelineCalendarEventService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
) {}
// TODO: Align return type with the entities to avoid mapping
async getCalendarEventsFromPersonIds({
currentWorkspaceMemberId,
personIds,
@@ -32,136 +32,141 @@ export class TimelineCalendarEventService {
page: number;
pageSize: number;
}): Promise<TimelineCalendarEventsWithTotalDTO> {
const offset = (page - 1) * pageSize;
const authContext = buildSystemAuthContext(workspaceId);
const calendarEventRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<CalendarEventWorkspaceEntity>(
workspaceId,
'calendarEvent',
);
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const offset = (page - 1) * pageSize;
const calendarEventIds = await calendarEventRepository.find({
where: {
calendarEventParticipants: {
personId: Any(personIds),
},
},
select: {
id: true,
startsAt: true,
},
skip: offset,
take: pageSize,
order: {
startsAt: 'DESC',
},
});
const calendarEventRepository =
await this.globalWorkspaceOrmManager.getRepository<CalendarEventWorkspaceEntity>(
workspaceId,
'calendarEvent',
);
const ids = calendarEventIds.map(({ id }) => id);
if (ids.length <= 0) {
return {
totalNumberOfCalendarEvents: 0,
timelineCalendarEvents: [],
};
}
// We've split the query into two parts, because we want to fetch all the participants without any filtering
const [events, total] = await calendarEventRepository.findAndCount({
where: {
id: Any(ids),
},
relations: {
calendarEventParticipants: {
person: true,
workspaceMember: true,
},
calendarChannelEventAssociations: {
calendarChannel: {
connectedAccount: {
accountOwner: true,
const calendarEventIds = await calendarEventRepository.find({
where: {
calendarEventParticipants: {
personId: Any(personIds),
},
},
},
},
});
select: {
id: true,
startsAt: true,
},
skip: offset,
take: pageSize,
order: {
startsAt: 'DESC',
},
});
// Keep events in the same order as they ids were returned
const orderedEvents = events.sort(
(a, b) => ids.indexOf(a.id) - ids.indexOf(b.id),
);
const ids = calendarEventIds.map(({ id }) => id);
const timelineCalendarEvents = orderedEvents.map((event) => {
const participants = event.calendarEventParticipants.map(
(participant) => ({
calendarEventId: event.id,
personId: participant.personId ?? null,
workspaceMemberId: participant.workspaceMemberId ?? null,
firstName:
participant.person?.name?.firstName ||
participant.workspaceMember?.name.firstName ||
'',
lastName:
participant.person?.name?.lastName ||
participant.workspaceMember?.name.lastName ||
'',
displayName:
participant.person?.name?.firstName ||
participant.person?.name?.lastName ||
participant.workspaceMember?.name.firstName ||
participant.workspaceMember?.name.lastName ||
participant.displayName ||
participant.handle ||
'',
avatarUrl:
participant.person?.avatarUrl ||
participant.workspaceMember?.avatarUrl ||
'',
handle: participant.handle ?? '',
}),
);
if (ids.length <= 0) {
return {
totalNumberOfCalendarEvents: 0,
timelineCalendarEvents: [],
};
}
const isCalendarEventImportedByCurrentWorkspaceMember =
event.calendarChannelEventAssociations.some(
(association) =>
association.calendarChannel.connectedAccount.accountOwnerId ===
currentWorkspaceMemberId,
const [events, total] = await calendarEventRepository.findAndCount({
where: {
id: Any(ids),
},
relations: {
calendarEventParticipants: {
person: true,
workspaceMember: true,
},
calendarChannelEventAssociations: {
calendarChannel: {
connectedAccount: {
accountOwner: true,
},
},
},
},
});
const orderedEvents = events.sort(
(a, b) => ids.indexOf(a.id) - ids.indexOf(b.id),
);
const visibility =
event.calendarChannelEventAssociations.some(
(association) =>
association.calendarChannel.visibility === 'SHARE_EVERYTHING',
) || isCalendarEventImportedByCurrentWorkspaceMember
? CalendarChannelVisibility.SHARE_EVERYTHING
: CalendarChannelVisibility.METADATA;
const timelineCalendarEvents = orderedEvents.map((event) => {
const participants = event.calendarEventParticipants.map(
(participant) => ({
calendarEventId: event.id,
personId: participant.personId ?? null,
workspaceMemberId: participant.workspaceMemberId ?? null,
firstName:
participant.person?.name?.firstName ||
participant.workspaceMember?.name.firstName ||
'',
lastName:
participant.person?.name?.lastName ||
participant.workspaceMember?.name.lastName ||
'',
displayName:
participant.person?.name?.firstName ||
participant.person?.name?.lastName ||
participant.workspaceMember?.name.firstName ||
participant.workspaceMember?.name.lastName ||
participant.displayName ||
participant.handle ||
'',
avatarUrl:
participant.person?.avatarUrl ||
participant.workspaceMember?.avatarUrl ||
'',
handle: participant.handle ?? '',
}),
);
return {
...omit(event, [
'calendarEventParticipants',
'calendarChannelEventAssociations',
]),
title:
visibility === CalendarChannelVisibility.METADATA
? FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED
: (event.title ?? ''),
description:
visibility === CalendarChannelVisibility.METADATA
? FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED
: (event.description ?? ''),
startsAt: event.startsAt as unknown as Date,
endsAt: event.endsAt as unknown as Date,
participants,
visibility,
location: event.location ?? '',
conferenceSolution: event.conferenceSolution ?? '',
};
});
const isCalendarEventImportedByCurrentWorkspaceMember =
event.calendarChannelEventAssociations.some(
(association) =>
association.calendarChannel.connectedAccount.accountOwnerId ===
currentWorkspaceMemberId,
);
return {
totalNumberOfCalendarEvents: total,
timelineCalendarEvents,
};
const visibility =
event.calendarChannelEventAssociations.some(
(association) =>
association.calendarChannel.visibility === 'SHARE_EVERYTHING',
) || isCalendarEventImportedByCurrentWorkspaceMember
? CalendarChannelVisibility.SHARE_EVERYTHING
: CalendarChannelVisibility.METADATA;
return {
...omit(event, [
'calendarEventParticipants',
'calendarChannelEventAssociations',
]),
title:
visibility === CalendarChannelVisibility.METADATA
? FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED
: (event.title ?? ''),
description:
visibility === CalendarChannelVisibility.METADATA
? FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED
: (event.description ?? ''),
startsAt: event.startsAt as unknown as Date,
endsAt: event.endsAt as unknown as Date,
participants,
visibility,
location: event.location ?? '',
conferenceSolution: event.conferenceSolution ?? '',
};
});
return {
totalNumberOfCalendarEvents: total,
timelineCalendarEvents,
};
},
);
}
async getCalendarEventsFromCompanyId({
@@ -177,39 +182,46 @@ export class TimelineCalendarEventService {
page: number;
pageSize: number;
}): Promise<TimelineCalendarEventsWithTotalDTO> {
const personRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<PersonWorkspaceEntity>(
workspaceId,
'person',
);
const authContext = buildSystemAuthContext(workspaceId);
const personIds = await personRepository.find({
where: {
companyId,
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const personRepository =
await this.globalWorkspaceOrmManager.getRepository<PersonWorkspaceEntity>(
workspaceId,
'person',
);
const personIds = await personRepository.find({
where: {
companyId,
},
select: {
id: true,
},
});
if (personIds.length <= 0) {
return {
totalNumberOfCalendarEvents: 0,
timelineCalendarEvents: [],
};
}
const formattedPersonIds = personIds.map(({ id }) => id);
const calendarEvents = await this.getCalendarEventsFromPersonIds({
currentWorkspaceMemberId,
personIds: formattedPersonIds,
workspaceId,
page,
pageSize,
});
return calendarEvents;
},
select: {
id: true,
},
});
if (personIds.length <= 0) {
return {
totalNumberOfCalendarEvents: 0,
timelineCalendarEvents: [],
};
}
const formattedPersonIds = personIds.map(({ id }) => id);
const calendarEvents = await this.getCalendarEventsFromPersonIds({
currentWorkspaceMemberId,
personIds: formattedPersonIds,
workspaceId,
page,
pageSize,
});
return calendarEvents;
);
}
async getCalendarEventsFromOpportunityId({
@@ -225,36 +237,43 @@ export class TimelineCalendarEventService {
page: number;
pageSize: number;
}): Promise<TimelineCalendarEventsWithTotalDTO> {
const opportunityRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<OpportunityWorkspaceEntity>(
workspaceId,
'opportunity',
);
const authContext = buildSystemAuthContext(workspaceId);
const opportunity = await opportunityRepository.findOne({
where: {
id: opportunityId,
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const opportunityRepository =
await this.globalWorkspaceOrmManager.getRepository<OpportunityWorkspaceEntity>(
workspaceId,
'opportunity',
);
const opportunity = await opportunityRepository.findOne({
where: {
id: opportunityId,
},
select: {
companyId: true,
},
});
if (!opportunity?.companyId) {
return {
totalNumberOfCalendarEvents: 0,
timelineCalendarEvents: [],
};
}
const calendarEvents = await this.getCalendarEventsFromCompanyId({
currentWorkspaceMemberId,
companyId: opportunity.companyId,
workspaceId,
page,
pageSize,
});
return calendarEvents;
},
select: {
companyId: true,
},
});
if (!opportunity?.companyId) {
return {
totalNumberOfCalendarEvents: 0,
timelineCalendarEvents: [],
};
}
const calendarEvents = await this.getCalendarEventsFromCompanyId({
currentWorkspaceMemberId,
companyId: opportunity.companyId,
workspaceId,
page,
pageSize,
});
return calendarEvents;
);
}
}
@@ -7,9 +7,8 @@ import {
} from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { ConnectedAccountProvider } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { PermissionFlagType } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
@@ -27,9 +26,7 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { ImapSmtpCalDavAPIService } from 'src/modules/connected-account/services/imap-smtp-caldav-apis.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
@Resolver()
@UsePipes(ResolverValidationPipe)
@@ -37,7 +34,6 @@ import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-acco
@UseGuards(SettingsPermissionGuard(PermissionFlagType.WORKSPACE))
export class ImapSmtpCaldavResolver {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly ImapSmtpCaldavConnectionService: ImapSmtpCaldavService,
private readonly imapSmtpCaldavApisService: ImapSmtpCalDavAPIService,
private readonly featureFlagService: FeatureFlagService,
@@ -50,16 +46,12 @@ export class ImapSmtpCaldavResolver {
@Args('id', { type: () => UUIDScalarType }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<ConnectedImapSmtpCaldavAccountDTO> {
const connectedAccountRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ConnectedAccountWorkspaceEntity>(
const connectedAccount =
await this.imapSmtpCaldavApisService.getImapSmtpCaldavConnectedAccount(
workspace.id,
'connectedAccount',
id,
);
const connectedAccount = await connectedAccountRepository.findOne({
where: { id, provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV },
});
if (!isDefined(connectedAccount) || !isDefined(connectedAccount?.handle)) {
throw new UserInputError(
`Connected mail account with ID ${id} not found`,
@@ -10,7 +10,8 @@ import {
type AccountType,
type ConnectionParameters,
} from 'src/engine/core-modules/imap-smtp-caldav-connection/types/imap-smtp-caldav-connection.type';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.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 { CalDAVClient } from 'src/modules/calendar/calendar-event-import-manager/drivers/caldav/lib/caldav.client';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
@@ -19,7 +20,7 @@ export class ImapSmtpCaldavService {
private readonly logger = new Logger(ImapSmtpCaldavService.name);
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
) {}
async testImapConnection(
@@ -175,19 +176,26 @@ export class ImapSmtpCaldavService {
workspaceId: string,
connectionId: string,
): Promise<ConnectedAccountWorkspaceEntity | null> {
const connectedAccountRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const authContext = buildSystemAuthContext(workspaceId);
const connectedAccount = await connectedAccountRepository.findOne({
where: {
id: connectionId,
provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const connectedAccountRepository =
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const connectedAccount = await connectedAccountRepository.findOne({
where: {
id: connectionId,
provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
},
});
return connectedAccount;
},
});
return connectedAccount;
);
}
}
@@ -4,14 +4,15 @@ import { TIMELINE_THREADS_DEFAULT_PAGE_SIZE } from 'src/engine/core-modules/mess
import { type TimelineThreadsWithTotalDTO } from 'src/engine/core-modules/messaging/dtos/timeline-threads-with-total.dto';
import { TimelineMessagingService } from 'src/engine/core-modules/messaging/services/timeline-messaging.service';
import { formatThreads } from 'src/engine/core-modules/messaging/utils/format-threads.util';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.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 OpportunityWorkspaceEntity } from 'src/modules/opportunity/standard-objects/opportunity.workspace-entity';
import { type PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
@Injectable()
export class GetMessagesService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly timelineMessagingService: TimelineMessagingService,
) {}
@@ -73,38 +74,45 @@ export class GetMessagesService {
page = 1,
pageSize: number = TIMELINE_THREADS_DEFAULT_PAGE_SIZE,
): Promise<TimelineThreadsWithTotalDTO> {
const personRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<PersonWorkspaceEntity>(
workspaceId,
'person',
);
const personIds = (
await personRepository.find({
where: {
companyId,
},
select: {
id: true,
},
})
).map((person) => person.id);
const authContext = buildSystemAuthContext(workspaceId);
if (personIds.length === 0) {
return {
totalNumberOfThreads: 0,
timelineThreads: [],
};
}
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const personRepository =
await this.globalWorkspaceOrmManager.getRepository<PersonWorkspaceEntity>(
workspaceId,
'person',
);
const personIds = (
await personRepository.find({
where: {
companyId,
},
select: {
id: true,
},
})
).map((person) => person.id);
const messageThreads = await this.getMessagesFromPersonIds(
workspaceMemberId,
personIds,
workspaceId,
page,
pageSize,
if (personIds.length === 0) {
return {
totalNumberOfThreads: 0,
timelineThreads: [],
};
}
const messageThreads = await this.getMessagesFromPersonIds(
workspaceMemberId,
personIds,
workspaceId,
page,
pageSize,
);
return messageThreads;
},
);
return messageThreads;
}
async getMessagesFromOpportunityId(
@@ -114,36 +122,43 @@ export class GetMessagesService {
page = 1,
pageSize: number = TIMELINE_THREADS_DEFAULT_PAGE_SIZE,
): Promise<TimelineThreadsWithTotalDTO> {
const opportunityRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<OpportunityWorkspaceEntity>(
workspaceId,
'opportunity',
);
const authContext = buildSystemAuthContext(workspaceId);
const opportunity = await opportunityRepository.findOne({
where: {
id: opportunityId,
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const opportunityRepository =
await this.globalWorkspaceOrmManager.getRepository<OpportunityWorkspaceEntity>(
workspaceId,
'opportunity',
);
const opportunity = await opportunityRepository.findOne({
where: {
id: opportunityId,
},
select: {
companyId: true,
},
});
if (!opportunity?.companyId) {
return {
totalNumberOfThreads: 0,
timelineThreads: [],
};
}
const messageThreads = await this.getMessagesFromCompanyId(
workspaceMemberId,
opportunity.companyId,
workspaceId,
page,
pageSize,
);
return messageThreads;
},
select: {
companyId: true,
},
});
if (!opportunity?.companyId) {
return {
totalNumberOfThreads: 0,
timelineThreads: [],
};
}
const messageThreads = await this.getMessagesFromCompanyId(
workspaceMemberId,
opportunity.companyId,
workspaceId,
page,
pageSize,
);
return messageThreads;
}
}
@@ -1,10 +1,11 @@
import { Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import { MessageParticipantRole } from 'twenty-shared/types';
import { In } from 'typeorm';
import { type TimelineThreadDTO } from 'src/engine/core-modules/messaging/dtos/timeline-thread.dto';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.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 { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { type MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity';
import { type MessageThreadWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-thread.workspace-entity';
@@ -12,7 +13,7 @@ import { type MessageThreadWorkspaceEntity } from 'src/modules/messaging/common/
@Injectable()
export class TimelineMessagingService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
) {}
public async getAndCountMessageThreads(
@@ -31,63 +32,74 @@ export class TimelineMessagingService {
>[];
totalNumberOfThreads: number;
}> {
const messageThreadRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<MessageThreadWorkspaceEntity>(
workspaceId,
'messageThread',
);
const authContext = buildSystemAuthContext(workspaceId);
const totalNumberOfThreads = await messageThreadRepository
.createQueryBuilder('messageThread')
.innerJoin('messageThread.messages', 'messages')
.innerJoin('messages.messageParticipants', 'messageParticipants')
.where('messageParticipants.personId IN(:...personIds)', { personIds })
.groupBy('messageThread.id')
.getCount();
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const messageThreadRepository =
await this.globalWorkspaceOrmManager.getRepository<MessageThreadWorkspaceEntity>(
workspaceId,
'messageThread',
);
const threadIdsQuery = await messageThreadRepository
.createQueryBuilder('messageThread')
.select('messageThread.id', 'id')
.addSelect('MAX(messages.receivedAt)', 'max_received_at')
.innerJoin('messageThread.messages', 'messages')
.innerJoin('messages.messageParticipants', 'messageParticipants')
.where('messageParticipants.personId IN (:...personIds)', { personIds })
.groupBy('messageThread.id')
.orderBy('max_received_at', 'DESC')
.offset(offset)
.limit(pageSize)
.getRawMany();
const totalNumberOfThreads = await messageThreadRepository
.createQueryBuilder('messageThread')
.innerJoin('messageThread.messages', 'messages')
.innerJoin('messages.messageParticipants', 'messageParticipants')
.where('messageParticipants.personId IN(:...personIds)', {
personIds,
})
.groupBy('messageThread.id')
.getCount();
const messageThreadIds = threadIdsQuery.map((thread) => thread.id);
const threadIdsQuery = await messageThreadRepository
.createQueryBuilder('messageThread')
.select('messageThread.id', 'id')
.addSelect('MAX(messages.receivedAt)', 'max_received_at')
.innerJoin('messageThread.messages', 'messages')
.innerJoin('messages.messageParticipants', 'messageParticipants')
.where('messageParticipants.personId IN (:...personIds)', {
personIds,
})
.groupBy('messageThread.id')
.orderBy('max_received_at', 'DESC')
.offset(offset)
.limit(pageSize)
.getRawMany();
const messageThreads = await messageThreadRepository.find({
where: {
id: In(messageThreadIds),
},
order: {
messages: {
receivedAt: 'DESC',
},
},
relations: ['messages'],
});
const messageThreadIds = threadIdsQuery.map((thread) => thread.id);
return {
messageThreads: messageThreads.map((messageThread) => {
const lastMessage = messageThread.messages[0];
const firstMessage =
messageThread.messages[messageThread.messages.length - 1];
const messageThreads = await messageThreadRepository.find({
where: {
id: In(messageThreadIds),
},
order: {
messages: {
receivedAt: 'DESC',
},
},
relations: ['messages'],
});
return {
id: messageThread.id,
subject: firstMessage.subject ?? '',
lastMessageBody: lastMessage.text ?? '',
lastMessageReceivedAt: lastMessage.receivedAt ?? new Date(),
numberOfMessagesInThread: messageThread.messages.length,
messageThreads: messageThreads.map((messageThread) => {
const lastMessage = messageThread.messages[0];
const firstMessage =
messageThread.messages[messageThread.messages.length - 1];
return {
id: messageThread.id,
subject: firstMessage.subject ?? '',
lastMessageBody: lastMessage.text ?? '',
lastMessageReceivedAt: lastMessage.receivedAt ?? new Date(),
numberOfMessagesInThread: messageThread.messages.length,
};
}),
totalNumberOfThreads,
};
}),
totalNumberOfThreads,
};
},
);
}
public async getThreadParticipantsByThreadId(
@@ -96,89 +108,96 @@ export class TimelineMessagingService {
): Promise<{
[key: string]: MessageParticipantWorkspaceEntity[];
}> {
const messageParticipantRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<MessageParticipantWorkspaceEntity>(
workspaceId,
'messageParticipant',
);
const authContext = buildSystemAuthContext(workspaceId);
const threadParticipants = await messageParticipantRepository
.createQueryBuilder()
.select('messageParticipant')
.addSelect('message.messageThreadId')
.addSelect('message.receivedAt')
.leftJoinAndSelect('messageParticipant.person', 'person')
.leftJoinAndSelect(
'messageParticipant.workspaceMember',
'workspaceMember',
)
.leftJoin('messageParticipant.message', 'message')
.where('message.messageThreadId = ANY(:messageThreadIds)', {
messageThreadIds,
})
.andWhere('messageParticipant.role = :role', {
role: MessageParticipantRole.FROM,
})
.orderBy('message.messageThreadId')
.distinctOn(['message.messageThreadId', 'messageParticipant.handle'])
.getMany();
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const messageParticipantRepository =
await this.globalWorkspaceOrmManager.getRepository<MessageParticipantWorkspaceEntity>(
workspaceId,
'messageParticipant',
);
// This is because subqueries are not handled by twentyORM
const orderedThreadParticipants = threadParticipants.sort(
(a, b) =>
(a.message.receivedAt ?? new Date()).getTime() -
(b.message.receivedAt ?? new Date()).getTime(),
);
const threadParticipants = await messageParticipantRepository
.createQueryBuilder()
.select('messageParticipant')
.addSelect('message.messageThreadId')
.addSelect('message.receivedAt')
.leftJoinAndSelect('messageParticipant.person', 'person')
.leftJoinAndSelect(
'messageParticipant.workspaceMember',
'workspaceMember',
)
.leftJoin('messageParticipant.message', 'message')
.where('message.messageThreadId = ANY(:messageThreadIds)', {
messageThreadIds,
})
.andWhere('messageParticipant.role = :role', {
role: MessageParticipantRole.FROM,
})
.orderBy('message.messageThreadId')
.distinctOn(['message.messageThreadId', 'messageParticipant.handle'])
.getMany();
// This is because composite fields are not handled correctly by the ORM
const threadParticipantsWithCompositeFields = orderedThreadParticipants.map(
(threadParticipant) => ({
...threadParticipant,
person: {
id: threadParticipant.person?.id,
name: {
//eslint-disable-next-line
//@ts-ignore
firstName: threadParticipant.person?.nameFirstName,
//eslint-disable-next-line
//@ts-ignore
lastName: threadParticipant.person?.nameLastName,
},
avatarUrl: threadParticipant.person?.avatarUrl,
},
workspaceMember: {
id: threadParticipant.workspaceMember?.id,
name: {
//eslint-disable-next-line
//@ts-ignore
firstName: threadParticipant.workspaceMember?.nameFirstName,
//eslint-disable-next-line
//@ts-ignore
lastName: threadParticipant.workspaceMember?.nameLastName,
},
avatarUrl: threadParticipant.workspaceMember?.avatarUrl,
},
}),
);
return threadParticipantsWithCompositeFields.reduce(
(threadParticipantsAcc, threadParticipant) => {
if (!threadParticipant.message.messageThreadId)
return threadParticipantsAcc;
// @ts-expect-error legacy noImplicitAny
if (!threadParticipantsAcc[threadParticipant.message.messageThreadId])
// @ts-expect-error legacy noImplicitAny
threadParticipantsAcc[threadParticipant.message.messageThreadId] = [];
// @ts-expect-error legacy noImplicitAny
threadParticipantsAcc[threadParticipant.message.messageThreadId].push(
threadParticipant,
const orderedThreadParticipants = threadParticipants.sort(
(a, b) =>
(a.message.receivedAt ?? new Date()).getTime() -
(b.message.receivedAt ?? new Date()).getTime(),
);
return threadParticipantsAcc;
const threadParticipantsWithCompositeFields =
orderedThreadParticipants.map((threadParticipant) => ({
...threadParticipant,
person: {
id: threadParticipant.person?.id,
name: {
//eslint-disable-next-line
//@ts-ignore
firstName: threadParticipant.person?.nameFirstName,
//eslint-disable-next-line
//@ts-ignore
lastName: threadParticipant.person?.nameLastName,
},
avatarUrl: threadParticipant.person?.avatarUrl,
},
workspaceMember: {
id: threadParticipant.workspaceMember?.id,
name: {
//eslint-disable-next-line
//@ts-ignore
firstName: threadParticipant.workspaceMember?.nameFirstName,
//eslint-disable-next-line
//@ts-ignore
lastName: threadParticipant.workspaceMember?.nameLastName,
},
avatarUrl: threadParticipant.workspaceMember?.avatarUrl,
},
}));
return threadParticipantsWithCompositeFields.reduce(
(threadParticipantsAcc, threadParticipant) => {
if (!threadParticipant.message.messageThreadId)
return threadParticipantsAcc;
if (
// @ts-expect-error legacy noImplicitAny
!threadParticipantsAcc[threadParticipant.message.messageThreadId]
)
// @ts-expect-error legacy noImplicitAny
threadParticipantsAcc[threadParticipant.message.messageThreadId] =
[];
// @ts-expect-error legacy noImplicitAny
threadParticipantsAcc[
threadParticipant.message.messageThreadId
].push(threadParticipant);
return threadParticipantsAcc;
},
{},
);
},
{},
);
}
@@ -189,58 +208,65 @@ export class TimelineMessagingService {
): Promise<{
[key: string]: MessageChannelVisibility;
}> {
const messageThreadRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<MessageThreadWorkspaceEntity>(
workspaceId,
'messageThread',
);
const authContext = buildSystemAuthContext(workspaceId);
const threadVisibility = await messageThreadRepository
.createQueryBuilder()
.select('messageThread.id', 'id')
.addSelect('messageChannel.visibility', 'visibility')
.addSelect('connectedAccount.accountOwnerId', 'accountOwnerId')
.leftJoin('messageThread.messages', 'message')
.leftJoin(
'message.messageChannelMessageAssociations',
'messageChannelMessageAssociation',
)
.leftJoin(
'messageChannelMessageAssociation.messageChannel',
'messageChannel',
)
.leftJoin('messageChannel.connectedAccount', 'connectedAccount')
.where('messageThread.id = ANY(:messageThreadIds)', {
messageThreadIds: messageThreadIds,
})
.getRawMany();
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const messageThreadRepository =
await this.globalWorkspaceOrmManager.getRepository<MessageThreadWorkspaceEntity>(
workspaceId,
'messageThread',
);
const visibilityValues = Object.values(MessageChannelVisibility);
const threadVisibilityByThreadId: {
[key: string]: MessageChannelVisibility;
} = threadVisibility.reduce((threadVisibilityAcc, threadVisibility) => {
if (threadVisibility.accountOwnerId === workspaceMemberId) {
threadVisibilityAcc[threadVisibility.id] =
MessageChannelVisibility.SHARE_EVERYTHING;
return threadVisibilityAcc;
}
threadVisibilityAcc[threadVisibility.id] =
visibilityValues[
Math.max(
visibilityValues.indexOf(threadVisibility.visibility),
visibilityValues.indexOf(
threadVisibilityAcc[threadVisibility.id] ??
MessageChannelVisibility.METADATA,
),
const threadVisibility = await messageThreadRepository
.createQueryBuilder()
.select('messageThread.id', 'id')
.addSelect('messageChannel.visibility', 'visibility')
.addSelect('connectedAccount.accountOwnerId', 'accountOwnerId')
.leftJoin('messageThread.messages', 'message')
.leftJoin(
'message.messageChannelMessageAssociations',
'messageChannelMessageAssociation',
)
];
.leftJoin(
'messageChannelMessageAssociation.messageChannel',
'messageChannel',
)
.leftJoin('messageChannel.connectedAccount', 'connectedAccount')
.where('messageThread.id = ANY(:messageThreadIds)', {
messageThreadIds: messageThreadIds,
})
.getRawMany();
return threadVisibilityAcc;
}, {});
const visibilityValues = Object.values(MessageChannelVisibility);
return threadVisibilityByThreadId;
const threadVisibilityByThreadId: {
[key: string]: MessageChannelVisibility;
} = threadVisibility.reduce((threadVisibilityAcc, threadVisibility) => {
if (threadVisibility.accountOwnerId === workspaceMemberId) {
threadVisibilityAcc[threadVisibility.id] =
MessageChannelVisibility.SHARE_EVERYTHING;
return threadVisibilityAcc;
}
threadVisibilityAcc[threadVisibility.id] =
visibilityValues[
Math.max(
visibilityValues.indexOf(threadVisibility.visibility),
visibilityValues.indexOf(
threadVisibilityAcc[threadVisibility.id] ??
MessageChannelVisibility.METADATA,
),
)
];
return threadVisibilityAcc;
}, {});
return threadVisibilityByThreadId;
},
);
}
}
@@ -16,14 +16,15 @@ import { RecordInputTransformerService } from 'src/engine/core-modules/record-tr
import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.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';
@Injectable()
export class CreateRecordService {
private readonly logger = new Logger(CreateRecordService.name);
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly recordPositionService: RecordPositionService,
private readonly recordInputTransformerService: RecordInputTransformerService,
) {}
@@ -40,116 +41,124 @@ export class CreateRecordService {
};
}
const authContext = buildSystemAuthContext(workspaceId);
try {
const repository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
objectName,
rolePermissionConfig,
);
return await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const repository = await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
objectName,
rolePermissionConfig,
);
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
} = repository.internalContext;
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
} = repository.internalContext;
const objectId = objectIdByNameSingular[objectName];
const objectId = objectIdByNameSingular[objectName];
if (!isDefined(objectId)) {
throw new RecordCrudException(
`Object ${objectName} not found`,
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
if (!isDefined(objectId)) {
throw new RecordCrudException(
`Object ${objectName} not found`,
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatObjectMetadataMaps,
flatEntityId: objectId,
});
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatObjectMetadataMaps,
flatEntityId: objectId,
});
if (
!canObjectBeManagedByWorkflow({
nameSingular: flatObjectMetadata.nameSingular,
isSystem: flatObjectMetadata.isSystem,
})
) {
throw new RecordCrudException(
'Failed to create: Object cannot be created by workflow',
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
if (
!canObjectBeManagedByWorkflow({
nameSingular: flatObjectMetadata.nameSingular,
isSystem: flatObjectMetadata.isSystem,
})
) {
throw new RecordCrudException(
'Failed to create: Object cannot be created by workflow',
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
const position = await this.recordPositionService.buildRecordPosition({
value: 'first',
objectMetadata: flatObjectMetadata,
workspaceId,
});
const position = await this.recordPositionService.buildRecordPosition(
{
value: 'first',
objectMetadata: flatObjectMetadata,
workspaceId,
},
);
const { fieldIdByName, fieldIdByJoinColumnName } =
buildFieldMapsFromFlatObjectMetadata(
flatFieldMetadataMaps,
flatObjectMetadata,
);
const { fieldIdByName, fieldIdByJoinColumnName } =
buildFieldMapsFromFlatObjectMetadata(
flatFieldMetadataMaps,
flatObjectMetadata,
);
const validObjectRecord = Object.fromEntries(
Object.entries(objectRecord).filter(
([key]) =>
isDefined(fieldIdByName[key]) ||
isDefined(fieldIdByJoinColumnName[key]),
),
);
const validObjectRecord = Object.fromEntries(
Object.entries(objectRecord).filter(
([key]) =>
isDefined(fieldIdByName[key]) ||
isDefined(fieldIdByJoinColumnName[key]),
),
);
const transformedObjectRecord =
await this.recordInputTransformerService.process({
recordInput: validObjectRecord,
flatObjectMetadata,
flatFieldMetadataMaps,
});
const restrictedFields =
repository.objectRecordsPermissions?.[flatObjectMetadata.id]
?.restrictedFields;
const selectedColumns = getSelectedColumnsFromRestrictedFields(
restrictedFields,
flatObjectMetadata,
flatFieldMetadataMaps,
);
const insertResult = await repository.insert(
{
...transformedObjectRecord,
position,
createdBy: params.createdBy ?? {
source: FieldActorSource.WORKFLOW,
name: 'Workflow',
},
},
undefined,
selectedColumns,
);
const [createdRecord] = insertResult.generatedMaps;
this.logger.log(`Record created successfully in ${objectName}`);
return {
success: true,
message: `Record created successfully in ${objectName}`,
result: createdRecord,
recordReferences: [
{
objectNameSingular: objectName,
recordId: createdRecord.id,
displayName: getRecordDisplayName(
{ ...transformedObjectRecord, ...createdRecord },
const transformedObjectRecord =
await this.recordInputTransformerService.process({
recordInput: validObjectRecord,
flatObjectMetadata,
flatFieldMetadataMaps,
),
},
],
};
});
const restrictedFields =
repository.objectRecordsPermissions?.[flatObjectMetadata.id]
?.restrictedFields;
const selectedColumns = getSelectedColumnsFromRestrictedFields(
restrictedFields,
flatObjectMetadata,
flatFieldMetadataMaps,
);
const insertResult = await repository.insert(
{
...transformedObjectRecord,
position,
createdBy: params.createdBy ?? {
source: FieldActorSource.WORKFLOW,
name: 'Workflow',
},
},
undefined,
selectedColumns,
);
const [createdRecord] = insertResult.generatedMaps;
this.logger.log(`Record created successfully in ${objectName}`);
return {
success: true,
message: `Record created successfully in ${objectName}`,
result: createdRecord,
recordReferences: [
{
objectNameSingular: objectName,
recordId: createdRecord.id,
displayName: getRecordDisplayName(
{ ...transformedObjectRecord, ...createdRecord },
flatObjectMetadata,
flatFieldMetadataMaps,
),
},
],
};
},
);
} catch (error) {
if (error instanceof RecordCrudException) {
return {
@@ -10,14 +10,15 @@ import {
import { type DeleteRecordParams } from 'src/engine/core-modules/record-crud/types/delete-record-params.type';
import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.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';
@Injectable()
export class DeleteRecordService {
private readonly logger = new Logger(DeleteRecordService.name);
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
) {}
async execute(params: DeleteRecordParams): Promise<ToolOutput> {
@@ -45,85 +46,93 @@ export class DeleteRecordService {
};
}
const authContext = buildSystemAuthContext(workspaceId);
try {
const repository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
objectName,
rolePermissionConfig,
);
return await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const repository = await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
objectName,
rolePermissionConfig,
);
const { flatObjectMetadataMaps, objectIdByNameSingular } =
repository.internalContext;
const { flatObjectMetadataMaps, objectIdByNameSingular } =
repository.internalContext;
const objectId = objectIdByNameSingular[objectName];
const objectId = objectIdByNameSingular[objectName];
if (!isDefined(objectId)) {
throw new RecordCrudException(
`Object ${objectName} not found`,
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
if (!isDefined(objectId)) {
throw new RecordCrudException(
`Object ${objectName} not found`,
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatObjectMetadataMaps,
flatEntityId: objectId,
});
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatObjectMetadataMaps,
flatEntityId: objectId,
});
if (
!canObjectBeManagedByWorkflow({
nameSingular: flatObjectMetadata.nameSingular,
isSystem: flatObjectMetadata.isSystem,
})
) {
throw new RecordCrudException(
'Failed to delete: Object cannot be deleted by workflow',
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
if (
!canObjectBeManagedByWorkflow({
nameSingular: flatObjectMetadata.nameSingular,
isSystem: flatObjectMetadata.isSystem,
})
) {
throw new RecordCrudException(
'Failed to delete: Object cannot be deleted by workflow',
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
const objectRecord = await repository.findOne({
where: {
id: objectRecordId,
const objectRecord = await repository.findOne({
where: {
id: objectRecordId,
},
});
if (!objectRecord) {
throw new RecordCrudException(
`Failed to delete: Record ${objectName} with id ${objectRecordId} not found`,
RecordCrudExceptionCode.RECORD_NOT_FOUND,
);
}
if (soft) {
const columnsToReturnForSoftDelete: string[] = [];
await repository.softDelete(
objectRecordId,
undefined,
columnsToReturnForSoftDelete,
);
this.logger.log(
`Record soft deleted successfully from ${objectName}`,
);
return {
success: true,
message: `Record soft deleted successfully from ${objectName}`,
result: objectRecord,
};
} else {
await repository.remove(objectRecord);
this.logger.log(
`Record permanently deleted successfully from ${objectName}`,
);
return {
success: true,
message: `Record permanently deleted successfully from ${objectName}`,
result: { id: objectRecordId },
};
}
},
});
if (!objectRecord) {
throw new RecordCrudException(
`Failed to delete: Record ${objectName} with id ${objectRecordId} not found`,
RecordCrudExceptionCode.RECORD_NOT_FOUND,
);
}
if (soft) {
const columnsToReturnForSoftDelete: string[] = [];
await repository.softDelete(
objectRecordId,
undefined,
columnsToReturnForSoftDelete,
);
this.logger.log(`Record soft deleted successfully from ${objectName}`);
return {
success: true,
message: `Record soft deleted successfully from ${objectName}`,
result: objectRecord,
};
} else {
await repository.remove(objectRecord);
this.logger.log(
`Record permanently deleted successfully from ${objectName}`,
);
return {
success: true,
message: `Record permanently deleted successfully from ${objectName}`,
result: { id: objectRecordId },
};
}
);
} catch (error) {
if (error instanceof RecordCrudException) {
return {
@@ -25,16 +25,17 @@ import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/typ
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { type WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
@Injectable()
export class FindRecordsService {
private readonly logger = new Logger(FindRecordsService.name);
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
) {}
async execute(
@@ -58,82 +59,88 @@ export class FindRecordsService {
};
}
const authContext = buildSystemAuthContext(workspaceId);
try {
const repository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
objectName,
rolePermissionConfig,
);
return await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const repository = await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
objectName,
rolePermissionConfig,
);
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
} = repository.internalContext;
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
} = repository.internalContext;
const objectId = objectIdByNameSingular[objectName];
const objectId = objectIdByNameSingular[objectName];
if (!isDefined(objectId)) {
throw new RecordCrudException(
`Object ${objectName} not found`,
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
if (!isDefined(objectId)) {
throw new RecordCrudException(
`Object ${objectName} not found`,
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatObjectMetadataMaps,
flatEntityId: objectId,
});
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatObjectMetadataMaps,
flatEntityId: objectId,
});
const graphqlQueryParser = new GraphqlQueryParser(
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
);
const graphqlQueryParser = new GraphqlQueryParser(
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
);
const records = await this.getObjectRecords({
objectName,
filter,
orderBy,
limit,
offset,
repository,
graphqlQueryParser,
flatObjectMetadata,
flatFieldMetadataMaps,
});
const records = await this.getObjectRecords({
objectName,
filter,
orderBy,
limit,
offset,
repository,
graphqlQueryParser,
flatObjectMetadata,
flatFieldMetadataMaps,
});
const totalCount = await this.getTotalCount({
objectName,
filter,
repository,
graphqlQueryParser,
flatObjectMetadata,
flatFieldMetadataMaps,
});
const totalCount = await this.getTotalCount({
objectName,
filter,
repository,
graphqlQueryParser,
flatObjectMetadata,
flatFieldMetadataMaps,
});
this.logger.log(`Found ${records.length} records in ${objectName}`);
this.logger.log(`Found ${records.length} records in ${objectName}`);
const recordReferences = records.map((record) => ({
objectNameSingular: objectName,
recordId: record.id as string,
displayName: getRecordDisplayName(
record,
flatObjectMetadata,
flatFieldMetadataMaps,
),
}));
const recordReferences = records.map((record) => ({
objectNameSingular: objectName,
recordId: record.id as string,
displayName: getRecordDisplayName(
record,
flatObjectMetadata,
flatFieldMetadataMaps,
),
}));
return {
success: true,
message: `Found ${records.length} ${objectName} records`,
result: {
records,
count: totalCount,
return {
success: true,
message: `Found ${records.length} ${objectName} records`,
result: {
records,
count: totalCount,
},
recordReferences,
};
},
recordReferences,
};
);
} catch (error) {
this.logger.error(`Failed to find records: ${error}`);
@@ -14,14 +14,15 @@ import { getSelectedColumnsFromRestrictedFields } from 'src/engine/core-modules/
import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service';
import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.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';
@Injectable()
export class UpdateRecordService {
private readonly logger = new Logger(UpdateRecordService.name);
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly recordInputTransformerService: RecordInputTransformerService,
) {}
@@ -51,135 +52,141 @@ export class UpdateRecordService {
};
}
const authContext = buildSystemAuthContext(workspaceId);
try {
const repository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
objectName,
rolePermissionConfig,
);
return await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const repository = await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
objectName,
rolePermissionConfig,
);
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
} = repository.internalContext;
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
} = repository.internalContext;
const objectId = objectIdByNameSingular[objectName];
const objectId = objectIdByNameSingular[objectName];
if (!isDefined(objectId)) {
throw new RecordCrudException(
`Object ${objectName} not found`,
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
if (!isDefined(objectId)) {
throw new RecordCrudException(
`Object ${objectName} not found`,
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatObjectMetadataMaps,
flatEntityId: objectId,
});
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatObjectMetadataMaps,
flatEntityId: objectId,
});
const restrictedFields =
repository.objectRecordsPermissions?.[flatObjectMetadata.id]
?.restrictedFields;
const restrictedFields =
repository.objectRecordsPermissions?.[flatObjectMetadata.id]
?.restrictedFields;
const selectedColumns = getSelectedColumnsFromRestrictedFields(
restrictedFields,
flatObjectMetadata,
flatFieldMetadataMaps,
);
const selectedColumns = getSelectedColumnsFromRestrictedFields(
restrictedFields,
flatObjectMetadata,
flatFieldMetadataMaps,
);
const previousObjectRecord = await repository.findOne({
where: {
id: objectRecordId,
},
select: selectedColumns,
});
const previousObjectRecord = await repository.findOne({
where: {
id: objectRecordId,
},
select: selectedColumns,
});
if (!previousObjectRecord) {
throw new RecordCrudException(
`Failed to update: Record ${objectName} with id ${objectRecordId} not found`,
RecordCrudExceptionCode.RECORD_NOT_FOUND,
);
}
if (!previousObjectRecord) {
throw new RecordCrudException(
`Failed to update: Record ${objectName} with id ${objectRecordId} not found`,
RecordCrudExceptionCode.RECORD_NOT_FOUND,
);
}
const fieldsToUpdateArray = fieldsToUpdate || Object.keys(objectRecord);
const fieldsToUpdateArray =
fieldsToUpdate || Object.keys(objectRecord);
if (fieldsToUpdateArray.length === 0) {
return {
success: true,
message: 'No fields to update',
result: previousObjectRecord,
};
}
if (
!canObjectBeManagedByWorkflow({
nameSingular: flatObjectMetadata.nameSingular,
isSystem: flatObjectMetadata.isSystem,
})
) {
throw new RecordCrudException(
'Failed to update: Object cannot be updated by workflow',
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
const objectRecordWithFilteredFields = Object.keys(objectRecord).reduce(
(acc, key) => {
if (fieldsToUpdateArray.includes(key)) {
if (fieldsToUpdateArray.length === 0) {
return {
...acc,
[key]: objectRecord[key],
success: true,
message: 'No fields to update',
result: previousObjectRecord,
};
}
return acc;
},
{},
);
if (
!canObjectBeManagedByWorkflow({
nameSingular: flatObjectMetadata.nameSingular,
isSystem: flatObjectMetadata.isSystem,
})
) {
throw new RecordCrudException(
'Failed to update: Object cannot be updated by workflow',
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
const transformedObjectRecord =
await this.recordInputTransformerService.process({
recordInput: objectRecordWithFilteredFields,
flatObjectMetadata,
flatFieldMetadataMaps,
});
const objectRecordWithFilteredFields = Object.keys(
objectRecord,
).reduce((acc, key) => {
if (fieldsToUpdateArray.includes(key)) {
return {
...acc,
[key]: objectRecord[key],
};
}
const updatedObjectRecord = {
...previousObjectRecord,
...objectRecordWithFilteredFields,
};
return acc;
}, {});
if (!deepEqual(updatedObjectRecord, previousObjectRecord)) {
await repository.update(
objectRecordId,
{
...transformedObjectRecord,
},
undefined,
selectedColumns,
);
}
this.logger.log(`Record updated successfully in ${objectName}`);
return {
success: true,
message: `Record updated successfully in ${objectName}`,
result: updatedObjectRecord,
recordReferences: [
{
objectNameSingular: objectName,
recordId: objectRecordId,
displayName: getRecordDisplayName(
updatedObjectRecord,
const transformedObjectRecord =
await this.recordInputTransformerService.process({
recordInput: objectRecordWithFilteredFields,
flatObjectMetadata,
flatFieldMetadataMaps,
),
},
],
};
});
const updatedObjectRecord = {
...previousObjectRecord,
...objectRecordWithFilteredFields,
};
if (!deepEqual(updatedObjectRecord, previousObjectRecord)) {
await repository.update(
objectRecordId,
{
...transformedObjectRecord,
},
undefined,
selectedColumns,
);
}
this.logger.log(`Record updated successfully in ${objectName}`);
return {
success: true,
message: `Record updated successfully in ${objectName}`,
result: updatedObjectRecord,
recordReferences: [
{
objectNameSingular: objectName,
recordId: objectRecordId,
displayName: getRecordDisplayName(
updatedObjectRecord,
flatObjectMetadata,
flatFieldMetadataMaps,
),
},
],
};
},
);
} catch (error) {
if (error instanceof RecordCrudException) {
return {
@@ -16,14 +16,15 @@ import { getCompositeTypeOrThrow } from 'src/engine/metadata-modules/field-metad
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.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';
@Injectable()
export class UpsertRecordService {
private readonly logger = new Logger(UpsertRecordService.name);
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly recordInputTransformerService: RecordInputTransformerService,
) {}
@@ -39,163 +40,167 @@ export class UpsertRecordService {
};
}
const authContext = buildSystemAuthContext(workspaceId);
try {
const repository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
objectName,
rolePermissionConfig,
);
return await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const repository = await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
objectName,
rolePermissionConfig,
);
const fieldsToUpdateArray = Object.keys(objectRecord).filter((field) =>
isDefined(objectRecord[field]),
);
const fieldsToUpdateArray = Object.keys(objectRecord).filter(
(field) => isDefined(objectRecord[field]),
);
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
} = repository.internalContext;
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
} = repository.internalContext;
const objectId = objectIdByNameSingular[objectName];
const objectId = objectIdByNameSingular[objectName];
if (!isDefined(objectId)) {
throw new RecordCrudException(
`Object ${objectName} not found`,
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatObjectMetadataMaps,
flatEntityId: objectId,
});
if (
!canObjectBeManagedByWorkflow({
nameSingular: flatObjectMetadata.nameSingular,
isSystem: flatObjectMetadata.isSystem,
})
) {
throw new RecordCrudException(
'Failed to update: Object cannot be updated by workflow',
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
const objectRecordWithFilteredFields = Object.keys(objectRecord).reduce(
(acc, key) => {
if (fieldsToUpdateArray.includes(key)) {
return {
...acc,
[key]: objectRecord[key],
};
}
return acc;
},
{},
);
const transformedObjectRecord =
await this.recordInputTransformerService.process({
recordInput: objectRecordWithFilteredFields,
flatObjectMetadata,
flatFieldMetadataMaps,
});
const { fieldIdByName, fieldIdByJoinColumnName } =
buildFieldMapsFromFlatObjectMetadata(
flatFieldMetadataMaps,
flatObjectMetadata,
);
const uniqueFieldsToUpdate = fieldsToUpdateArray
.map(
(fieldName) =>
fieldIdByName[fieldName] || fieldIdByJoinColumnName[fieldName],
)
.map((fieldId) => flatFieldMetadataMaps.byId[fieldId])
.filter(isDefined)
.filter((field) => field.isUnique || field.name === 'id');
const conflictPathsUniqueFieldsToUpdate = uniqueFieldsToUpdate.flatMap(
(field) => {
if (isCompositeFieldMetadataType(field.type)) {
const compositeType = getCompositeTypeOrThrow(field.type);
const uniqueProperties = compositeType.properties.filter(
(prop) => prop.isIncludedInUniqueConstraint,
);
const propertiesToUse =
uniqueProperties.length > 0
? uniqueProperties
: [compositeType.properties[0]];
return propertiesToUse.map((prop) =>
computeCompositeColumnName(field, prop),
if (!isDefined(objectId)) {
throw new RecordCrudException(
`Object ${objectName} not found`,
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
return [field.name];
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatObjectMetadataMaps,
flatEntityId: objectId,
});
if (
!canObjectBeManagedByWorkflow({
nameSingular: flatObjectMetadata.nameSingular,
isSystem: flatObjectMetadata.isSystem,
})
) {
throw new RecordCrudException(
'Failed to update: Object cannot be updated by workflow',
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
const objectRecordWithFilteredFields = Object.keys(
objectRecord,
).reduce((acc, key) => {
if (fieldsToUpdateArray.includes(key)) {
return {
...acc,
[key]: objectRecord[key],
};
}
return acc;
}, {});
const transformedObjectRecord =
await this.recordInputTransformerService.process({
recordInput: objectRecordWithFilteredFields,
flatObjectMetadata,
flatFieldMetadataMaps,
});
const { fieldIdByName, fieldIdByJoinColumnName } =
buildFieldMapsFromFlatObjectMetadata(
flatFieldMetadataMaps,
flatObjectMetadata,
);
const uniqueFieldsToUpdate = fieldsToUpdateArray
.map(
(fieldName) =>
fieldIdByName[fieldName] || fieldIdByJoinColumnName[fieldName],
)
.map((fieldId) => flatFieldMetadataMaps.byId[fieldId])
.filter(isDefined)
.filter((field) => field.isUnique || field.name === 'id');
const conflictPathsUniqueFieldsToUpdate =
uniqueFieldsToUpdate.flatMap((field) => {
if (isCompositeFieldMetadataType(field.type)) {
const compositeType = getCompositeTypeOrThrow(field.type);
const uniqueProperties = compositeType.properties.filter(
(prop) => prop.isIncludedInUniqueConstraint,
);
const propertiesToUse =
uniqueProperties.length > 0
? uniqueProperties
: [compositeType.properties[0]];
return propertiesToUse.map((prop) =>
computeCompositeColumnName(field, prop),
);
}
return [field.name];
});
const conflictPaths =
conflictPathsUniqueFieldsToUpdate.length > 0
? conflictPathsUniqueFieldsToUpdate
: ['id'];
const restrictedFields =
repository.objectRecordsPermissions?.[flatObjectMetadata.id]
?.restrictedFields;
const selectedColumns = getSelectedColumnsFromRestrictedFields(
restrictedFields,
flatObjectMetadata,
flatFieldMetadataMaps,
);
const upsertResult = await repository.upsert(
transformedObjectRecord,
{
conflictPaths: conflictPaths,
},
undefined,
selectedColumns,
);
const upsertedRecordId = upsertResult.identifiers?.[0].id;
if (!isDefined(upsertedRecordId)) {
throw new RecordCrudException(
`Failed to upsert record in ${objectName}`,
RecordCrudExceptionCode.RECORD_UPSERT_FAILED,
);
}
const upsertedRecord = await repository.findOne({
where: {
id: upsertedRecordId,
},
select: selectedColumns,
});
if (!upsertedRecord) {
throw new RecordCrudException(
`Record not found after upsert with id ${upsertedRecordId} in ${objectName}`,
RecordCrudExceptionCode.RECORD_UPSERT_FAILED,
);
}
this.logger.log(`Record upserted successfully in ${objectName}`);
return {
success: true,
message: `Record upserted successfully in ${objectName}`,
result: upsertedRecord,
};
},
);
const conflictPaths =
conflictPathsUniqueFieldsToUpdate.length > 0
? conflictPathsUniqueFieldsToUpdate
: ['id'];
const restrictedFields =
repository.objectRecordsPermissions?.[flatObjectMetadata.id]
?.restrictedFields;
const selectedColumns = getSelectedColumnsFromRestrictedFields(
restrictedFields,
flatObjectMetadata,
flatFieldMetadataMaps,
);
const upsertResult = await repository.upsert(
transformedObjectRecord,
{
conflictPaths: conflictPaths,
},
undefined,
selectedColumns,
);
const upsertedRecordId = upsertResult.identifiers?.[0].id;
if (!isDefined(upsertedRecordId)) {
throw new RecordCrudException(
`Failed to upsert record in ${objectName}`,
RecordCrudExceptionCode.RECORD_UPSERT_FAILED,
);
}
const upsertedRecord = await repository.findOne({
where: {
id: upsertedRecordId,
},
select: selectedColumns,
});
if (!upsertedRecord) {
throw new RecordCrudException(
`Record not found after upsert with id ${upsertedRecordId} in ${objectName}`,
RecordCrudExceptionCode.RECORD_UPSERT_FAILED,
);
}
this.logger.log(`Record upserted successfully in ${objectName}`);
return {
success: true,
message: `Record upserted successfully in ${objectName}`,
result: upsertedRecord,
};
} catch (error) {
if (error instanceof RecordCrudException) {
return {
@@ -1,10 +1,10 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
describe('RecordPositionService', () => {
let twentyORMGlobalManager: jest.Mocked<TwentyORMGlobalManager>;
let globalWorkspaceOrmManager: jest.Mocked<GlobalWorkspaceOrmManager>;
let mockRepository: any;
let service: RecordPositionService;
@@ -16,16 +16,19 @@ describe('RecordPositionService', () => {
maximum: jest.fn().mockResolvedValue(1),
};
twentyORMGlobalManager = {
getRepositoryForWorkspace: jest.fn().mockResolvedValue(mockRepository),
} as unknown as jest.Mocked<TwentyORMGlobalManager>;
globalWorkspaceOrmManager = {
getRepository: jest.fn().mockResolvedValue(mockRepository),
executeInWorkspaceContext: jest
.fn()
.mockImplementation((_authContext: any, fn: () => any) => fn()),
} as unknown as jest.Mocked<GlobalWorkspaceOrmManager>;
const module: TestingModule = await Test.createTestingModule({
providers: [
RecordPositionService,
{
provide: TwentyORMGlobalManager,
useValue: twentyORMGlobalManager,
provide: GlobalWorkspaceOrmManager,
useValue: globalWorkspaceOrmManager,
},
],
}).compile();
@@ -4,7 +4,8 @@ import { isNumber } from '@sniptt/guards';
import { type ObjectRecord } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.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';
export type RecordPositionServiceCreateArgs = {
value: number | 'first' | 'last';
@@ -16,7 +17,7 @@ export type RecordPositionServiceCreateArgs = {
@Injectable()
export class RecordPositionService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
) {}
async buildRecordPosition({
@@ -153,20 +154,26 @@ export class RecordPositionService {
objectMetadata: { isCustom: boolean; nameSingular: string },
workspaceId: string,
): Promise<{ id: string; position: number } | null> {
const repository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
objectMetadata.nameSingular,
{
shouldBypassPermissionChecks: true,
},
);
const authContext = buildSystemAuthContext(workspaceId);
const record = await repository.findOneBy({
position: positionValue,
});
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const repository = await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
objectMetadata.nameSingular,
{
shouldBypassPermissionChecks: true,
},
);
return record ? { id: record.id, position: record.position } : null;
const record = await repository.findOneBy({
position: positionValue,
});
return record ? { id: record.id, position: record.position } : null;
},
);
}
async updatePosition(
@@ -175,49 +182,67 @@ export class RecordPositionService {
objectMetadata: { isCustom: boolean; nameSingular: string },
workspaceId: string,
): Promise<void> {
const repository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
objectMetadata.nameSingular,
{
shouldBypassPermissionChecks: true,
},
);
const authContext = buildSystemAuthContext(workspaceId);
await repository.update(recordId, {
position: positionValue,
});
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const repository = await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
objectMetadata.nameSingular,
{
shouldBypassPermissionChecks: true,
},
);
await repository.update(recordId, {
position: positionValue,
});
},
);
}
private async findMinPosition(
objectMetadata: { isCustom: boolean; nameSingular: string },
workspaceId: string,
): Promise<number | null> {
const repository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
objectMetadata.nameSingular,
{
shouldBypassPermissionChecks: true,
},
);
const authContext = buildSystemAuthContext(workspaceId);
return await repository.minimum('position');
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const repository = await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
objectMetadata.nameSingular,
{
shouldBypassPermissionChecks: true,
},
);
return await repository.minimum('position');
},
);
}
private async findMaxPosition(
objectMetadata: { isCustom: boolean; nameSingular: string },
workspaceId: string,
): Promise<number | null> {
const repository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
objectMetadata.nameSingular,
{
shouldBypassPermissionChecks: true,
},
);
const authContext = buildSystemAuthContext(workspaceId);
return await repository.maximum('position');
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const repository = await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
objectMetadata.nameSingular,
{
shouldBypassPermissionChecks: true,
},
);
return await repository.maximum('position');
},
);
}
}
@@ -7,7 +7,7 @@ import {
} from 'src/engine/core-modules/__mocks__/mockFlatObjectMetadatas';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { SearchService } from 'src/engine/core-modules/search/services/search.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
describe('SearchService', () => {
@@ -17,7 +17,7 @@ describe('SearchService', () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
SearchService,
{ provide: TwentyORMGlobalManager, useValue: {} },
{ provide: GlobalWorkspaceOrmManager, useValue: {} },
{ provide: WorkspaceCacheStorageService, useValue: {} },
{ provide: FileService, useValue: {} },
],
@@ -30,9 +30,10 @@ import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/typ
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
type LastRanks = { tsRankCD: number; tsRank: number };
@@ -46,7 +47,7 @@ const OBJECT_METADATA_ITEMS_CHUNK_SIZE = 5;
@Injectable()
export class SearchService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly fileService: FileService,
) {}
@@ -81,29 +82,36 @@ export class SearchService {
OBJECT_METADATA_ITEMS_CHUNK_SIZE,
);
const authContext = buildSystemAuthContext(workspaceId);
for (const objectMetadataItemChunk of filteredObjectMetadataItemsChunks) {
const recordsWithObjectMetadataItems = await Promise.all(
objectMetadataItemChunk.map(async (flatObjectMetadata) => {
const repository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ObjectRecord>(
workspaceId,
flatObjectMetadata.nameSingular,
rolePermissionConfig,
);
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const repository =
await this.globalWorkspaceOrmManager.getRepository<ObjectRecord>(
workspaceId,
flatObjectMetadata.nameSingular,
rolePermissionConfig,
);
return {
objectMetadataItem: flatObjectMetadata,
records: await this.buildSearchQueryAndGetRecords({
entityManager: repository,
flatObjectMetadata,
flatFieldMetadataMaps,
searchTerms: formatSearchTerms(searchInput, 'and'),
searchTermsOr: formatSearchTerms(searchInput, 'or'),
limit: limit as number,
filter: filter ?? ({} as ObjectRecordFilter),
after,
}),
};
return {
objectMetadataItem: flatObjectMetadata,
records: await this.buildSearchQueryAndGetRecords({
entityManager: repository,
flatObjectMetadata,
flatFieldMetadataMaps,
searchTerms: formatSearchTerms(searchInput, 'and'),
searchTermsOr: formatSearchTerms(searchInput, 'or'),
limit: limit as number,
filter: filter ?? ({} as ObjectRecordFilter),
after,
}),
};
},
);
}),
);
@@ -9,6 +9,7 @@ import { In, Repository } from 'typeorm';
import { z } from 'zod';
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { extractFolderPathAndFilename } from 'src/engine/core-modules/file/utils/extract-folderpath-and-filename.utils';
import {
SendEmailToolException,
@@ -18,13 +19,13 @@ import { SendEmailToolParametersZodSchema } from 'src/engine/core-modules/tool/t
import { type SendEmailInput } from 'src/engine/core-modules/tool/tools/send-email-tool/types/send-email-input.type';
import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
import { type Tool } from 'src/engine/core-modules/tool/types/tool.type';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.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';
import { MessagingSendMessageService } from 'src/modules/messaging/message-import-manager/services/messaging-send-message.service';
import { type MessageAttachment } from 'src/modules/messaging/message-import-manager/types/message';
import { parseEmailBody } from 'src/utils/parse-email-body';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
import { type MessageAttachment } from 'src/modules/messaging/message-import-manager/types/message';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
@Injectable()
export class SendEmailTool implements Tool {
@@ -35,7 +36,7 @@ export class SendEmailTool implements Tool {
inputSchema = SendEmailToolParametersZodSchema;
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly sendMessageService: MessagingSendMessageService,
@InjectRepository(FileEntity)
private readonly fileRepository: Repository<FileEntity>,
@@ -53,49 +54,63 @@ export class SendEmailTool implements Tool {
);
}
const connectedAccountRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const authContext = buildSystemAuthContext(workspaceId);
const connectedAccount = await connectedAccountRepository.findOne({
where: { id: connectedAccountId },
relations: {
messageChannels: {
messageFolders: true,
},
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const connectedAccountRepository =
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const connectedAccount = await connectedAccountRepository.findOne({
where: { id: connectedAccountId },
relations: {
messageChannels: {
messageFolders: true,
},
},
});
if (!isDefined(connectedAccount)) {
throw new SendEmailToolException(
`Connected Account '${connectedAccountId}' not found`,
SendEmailToolExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND,
);
}
return connectedAccount;
},
});
if (!isDefined(connectedAccount)) {
throw new SendEmailToolException(
`Connected Account '${connectedAccountId}' not found`,
SendEmailToolExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND,
);
}
return connectedAccount;
);
}
private async getOrThrowFirstConnectedAccountId(
workspaceId: string,
): Promise<string> {
const connectedAccountRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const allAccounts = await connectedAccountRepository.find();
const authContext = buildSystemAuthContext(workspaceId);
if (!allAccounts || allAccounts.length === 0) {
throw new SendEmailToolException(
'No connected accounts found for this workspace',
SendEmailToolExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND,
);
}
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const connectedAccountRepository =
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const allAccounts = await connectedAccountRepository.find();
return allAccounts[0].id;
if (!allAccounts || allAccounts.length === 0) {
throw new SendEmailToolException(
'No connected accounts found for this workspace',
SendEmailToolExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND,
);
}
return allAccounts[0].id;
},
);
}
private async getAttachments(
@@ -27,7 +27,8 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
import { PermissionsException } from 'src/engine/metadata-modules/permissions/permissions.exception';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
describe('UserWorkspaceService', () => {
let service: UserWorkspaceService;
@@ -35,7 +36,7 @@ describe('UserWorkspaceService', () => {
let userRepository: Repository<UserEntity>;
let workspaceInvitationService: WorkspaceInvitationService;
let approvedAccessDomainService: ApprovedAccessDomainService;
let twentyORMGlobalManager: TwentyORMGlobalManager;
let globalWorkspaceOrmManager: GlobalWorkspaceOrmManager;
let userRoleService: UserRoleService;
let fileService: FileService;
let fileUploadService: FileUploadService;
@@ -102,9 +103,12 @@ describe('UserWorkspaceService', () => {
},
},
{
provide: TwentyORMGlobalManager,
provide: GlobalWorkspaceOrmManager,
useValue: {
getRepositoryForWorkspace: jest.fn(),
executeInWorkspaceContext: jest
.fn()
.mockImplementation(async (_authContext, callback) => callback()),
getRepository: jest.fn(),
},
},
{
@@ -156,9 +160,14 @@ describe('UserWorkspaceService', () => {
approvedAccessDomainService = module.get<ApprovedAccessDomainService>(
ApprovedAccessDomainService,
);
twentyORMGlobalManager = module.get<TwentyORMGlobalManager>(
TwentyORMGlobalManager,
globalWorkspaceOrmManager = module.get<GlobalWorkspaceOrmManager>(
GlobalWorkspaceOrmManager,
);
(globalWorkspaceOrmManager.getRepository as jest.Mock).mockResolvedValue({
findOne: jest.fn(),
update: jest.fn(),
} as unknown as WorkspaceRepository<UserWorkspaceEntity>);
userRoleService = module.get<UserRoleService>(UserRoleService);
fileUploadService = module.get<FileUploadService>(FileUploadService);
onboardingService = module.get<OnboardingService>(OnboardingService);
@@ -374,7 +383,7 @@ describe('UserWorkspaceService', () => {
.mockResolvedValueOnce(undefined)
.mockResolvedValueOnce(workspaceMember);
jest
.spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace')
.spyOn(globalWorkspaceOrmManager, 'getRepository')
.mockResolvedValue(workspaceMemberRepository as any);
jest.spyOn(userWorkspaceRepository, 'findOneOrFail').mockResolvedValue({
@@ -902,7 +911,7 @@ describe('UserWorkspaceService', () => {
};
jest
.spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace')
.spyOn(globalWorkspaceOrmManager, 'getRepository')
.mockResolvedValue(workspaceMemberRepository as any);
const result = await service.getWorkspaceMemberOrThrow({
@@ -910,11 +919,13 @@ describe('UserWorkspaceService', () => {
workspaceId,
});
expect(
twentyORMGlobalManager.getRepositoryForWorkspace,
).toHaveBeenCalledWith(workspaceId, 'workspaceMember', {
shouldBypassPermissionChecks: true,
});
expect(globalWorkspaceOrmManager.getRepository).toHaveBeenCalledWith(
workspaceId,
'workspaceMember',
{
shouldBypassPermissionChecks: true,
},
);
expect(workspaceMemberRepository.findOne).toHaveBeenCalledWith({
where: {
id: workspaceMemberId,
@@ -931,7 +942,7 @@ describe('UserWorkspaceService', () => {
};
jest
.spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace')
.spyOn(globalWorkspaceOrmManager, 'getRepository')
.mockResolvedValue(workspaceMemberRepository as any);
await expect(
@@ -33,7 +33,8 @@ import {
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.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 { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { assert } from 'src/utils/assert';
import { getDomainNameByEmail } from 'src/utils/get-domain-name-by-email';
@@ -50,7 +51,7 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspaceEntit
private readonly workspaceDomainsService: WorkspaceDomainsService,
private readonly loginTokenService: LoginTokenService,
private readonly approvedAccessDomainService: ApprovedAccessDomainService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly userRoleService: UserRoleService,
private readonly fileUploadService: FileUploadService,
private readonly fileService: FileService,
@@ -92,41 +93,48 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspaceEntit
}
async createWorkspaceMember(workspaceId: string, user: UserEntity) {
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspaceId);
const userWorkspace = await this.userWorkspaceRepository.findOneOrFail({
where: {
userId: user.id,
workspaceId,
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const userWorkspace = await this.userWorkspaceRepository.findOneOrFail({
where: {
userId: user.id,
workspaceId,
},
});
await workspaceMemberRepository.insert({
name: {
firstName: user.firstName,
lastName: user.lastName,
},
colorScheme: 'System',
userId: user.id,
userEmail: user.email,
avatarUrl: userWorkspace.defaultAvatarUrl ?? '',
locale: (user.locale ?? SOURCE_LOCALE) as keyof typeof APP_LOCALES,
});
const workspaceMember = await workspaceMemberRepository.find({
where: {
userId: user.id,
},
});
assert(
workspaceMember?.length === 1,
`Error while creating workspace member ${user.email} on workspace ${workspaceId}`,
);
},
});
await workspaceMemberRepository.insert({
name: {
firstName: user.firstName,
lastName: user.lastName,
},
colorScheme: 'System',
userId: user.id,
userEmail: user.email,
avatarUrl: userWorkspace.defaultAvatarUrl ?? '',
locale: (user.locale ?? SOURCE_LOCALE) as keyof typeof APP_LOCALES,
});
const workspaceMember = await workspaceMemberRepository.find({
where: {
userId: user.id,
},
});
assert(
workspaceMember?.length === 1,
`Error while creating workspace member ${user.email} on workspace ${workspaceId}`,
);
}
@@ -343,24 +351,31 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspaceEntit
workspaceMemberId: string;
workspaceId: string;
}): Promise<WorkspaceMemberWorkspaceEntity> {
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspaceId);
const workspaceMember = await workspaceMemberRepository.findOne({
where: {
id: workspaceMemberId,
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const workspaceMember = await workspaceMemberRepository.findOne({
where: {
id: workspaceMemberId,
},
});
if (!isDefined(workspaceMember)) {
throw new Error('Workspace member not found');
}
return workspaceMember;
},
});
if (!isDefined(workspaceMember)) {
throw new Error('Workspace member not found');
}
return workspaceMember;
);
}
private async computeDefaultAvatarUrl(
@@ -4,7 +4,8 @@ import { Process } from 'src/engine/core-modules/message-queue/decorators/proces
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 { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.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 { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
export type UpdateWorkspaceMemberEmailJobData = {
@@ -21,7 +22,7 @@ export class UpdateWorkspaceMemberEmailJob {
constructor(
private readonly userWorkspaceService: UserWorkspaceService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
) {}
@Process(UpdateWorkspaceMemberEmailJob.name)
@@ -32,13 +33,23 @@ export class UpdateWorkspaceMemberEmailJob {
const workspace =
await this.userWorkspaceService.findFirstWorkspaceByUserId(userId);
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspace.id);
await workspaceMemberRepository.update({ userId }, { userEmail: email });
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
await workspaceMemberRepository.update(
{ userId },
{ userEmail: email },
);
},
);
}
}
@@ -20,15 +20,15 @@ import {
PermissionsExceptionCode,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
describe('UserService', () => {
let service: UserService;
let userRepository: Repository<UserEntity>;
let workspaceService: WorkspaceService;
let twentyORMGlobalManager: TwentyORMGlobalManager;
let globalWorkspaceOrmManager: GlobalWorkspaceOrmManager;
let userRoleService: UserRoleService;
const mockWorkspaceMemberRepo = {
@@ -70,9 +70,13 @@ describe('UserService', () => {
useValue: { add: jest.fn() },
},
{
provide: TwentyORMGlobalManager,
provide: GlobalWorkspaceOrmManager,
useValue: {
getRepositoryForWorkspace: jest.fn(),
getRepository: jest.fn(),
executeInWorkspaceContext: jest
.fn()
.mockImplementation((_authContext: any, fn: () => any) => fn()),
},
},
{
@@ -99,8 +103,8 @@ describe('UserService', () => {
getRepositoryToken(UserEntity),
);
userRoleService = module.get<UserRoleService>(UserRoleService);
twentyORMGlobalManager = module.get<TwentyORMGlobalManager>(
TwentyORMGlobalManager,
globalWorkspaceOrmManager = module.get<GlobalWorkspaceOrmManager>(
GlobalWorkspaceOrmManager,
);
workspaceService = module.get<WorkspaceService>(WorkspaceService);
});
@@ -115,9 +119,7 @@ describe('UserService', () => {
);
expect(res).toBeNull();
expect(
twentyORMGlobalManager.getRepositoryForWorkspace,
).not.toHaveBeenCalled();
expect(globalWorkspaceOrmManager.getRepository).not.toHaveBeenCalled();
});
it('fetches from workspace member repo when workspace active', async () => {
@@ -127,7 +129,7 @@ describe('UserService', () => {
} as WorkspaceMemberWorkspaceEntity);
jest
.spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace')
.spyOn(globalWorkspaceOrmManager, 'getRepository')
.mockResolvedValue(mockWorkspaceMemberRepo);
const res = await service.loadWorkspaceMember(
@@ -138,11 +140,13 @@ describe('UserService', () => {
} as WorkspaceEntity,
);
expect(
twentyORMGlobalManager.getRepositoryForWorkspace,
).toHaveBeenCalledWith('w1', 'workspaceMember', {
shouldBypassPermissionChecks: true,
});
expect(globalWorkspaceOrmManager.getRepository).toHaveBeenCalledWith(
'w1',
'workspaceMember',
{
shouldBypassPermissionChecks: true,
},
);
expect(mockWorkspaceMemberRepo.findOne).toHaveBeenCalledWith({
where: { userId: 'u1' },
});
@@ -158,9 +162,7 @@ describe('UserService', () => {
} as WorkspaceEntity);
expect(res).toEqual([]);
expect(
twentyORMGlobalManager.getRepositoryForWorkspace,
).not.toHaveBeenCalled();
expect(globalWorkspaceOrmManager.getRepository).not.toHaveBeenCalled();
});
it('fetches members withDeleted flag', async () => {
@@ -168,7 +170,7 @@ describe('UserService', () => {
.spyOn(mockWorkspaceMemberRepo, 'find')
.mockResolvedValue([{ id: 'wm1' } as WorkspaceMemberWorkspaceEntity]);
jest
.spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace')
.spyOn(globalWorkspaceOrmManager, 'getRepository')
.mockResolvedValue(mockWorkspaceMemberRepo);
const res = await service.loadWorkspaceMembers(
@@ -203,7 +205,7 @@ describe('UserService', () => {
{ id: 'wm-del' } as WorkspaceMemberWorkspaceEntity,
]);
jest
.spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace')
.spyOn(globalWorkspaceOrmManager, 'getRepository')
.mockResolvedValue(mockWorkspaceMemberRepo);
await service.loadDeletedWorkspaceMembersOnly({
@@ -323,7 +325,7 @@ describe('UserService', () => {
{ id: 'wm-2', userId: 'uX' } as WorkspaceMemberWorkspaceEntity,
]);
jest
.spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace')
.spyOn(globalWorkspaceOrmManager, 'getRepository')
.mockResolvedValue(mockWorkspaceMemberRepo);
jest
@@ -358,7 +360,7 @@ describe('UserService', () => {
.spyOn(mockWorkspaceMemberRepo, 'find')
.mockResolvedValue([wmForUser('u2')]);
jest
.spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace')
.spyOn(globalWorkspaceOrmManager, 'getRepository')
.mockResolvedValue(mockWorkspaceMemberRepo);
(userRepository.softDelete as jest.Mock).mockResolvedValue({
@@ -37,7 +37,8 @@ import {
PermissionsExceptionMessage,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.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 { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
@@ -48,7 +49,7 @@ export class UserService extends TypeOrmQueryService<UserEntity> {
private readonly workspaceDomainsService: WorkspaceDomainsService,
private readonly emailVerificationService: EmailVerificationService,
private readonly workspaceService: WorkspaceService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly userRoleService: UserRoleService,
private readonly userWorkspaceService: UserWorkspaceService,
@InjectMessageQueue(MessageQueue.workspaceQueue)
@@ -62,18 +63,25 @@ export class UserService extends TypeOrmQueryService<UserEntity> {
return null;
}
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspace.id);
return await workspaceMemberRepository.findOne({
where: {
userId: user.id,
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
return await workspaceMemberRepository.findOne({
where: {
userId: user.id,
},
});
},
});
);
}
async loadWorkspaceMembers(workspace: WorkspaceEntity, withDeleted = false) {
@@ -81,14 +89,23 @@ export class UserService extends TypeOrmQueryService<UserEntity> {
return [];
}
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspace.id);
return await workspaceMemberRepository.find({ withDeleted: withDeleted });
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
return await workspaceMemberRepository.find({
withDeleted: withDeleted,
});
},
);
}
async loadDeletedWorkspaceMembersOnly(workspace: WorkspaceEntity) {
@@ -96,17 +113,24 @@ export class UserService extends TypeOrmQueryService<UserEntity> {
return [];
}
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspace.id);
return await workspaceMemberRepository.find({
where: { deletedAt: Not(IsNull()) },
withDeleted: true,
});
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
return await workspaceMemberRepository.find({
where: { deletedAt: Not(IsNull()) },
withDeleted: true,
});
},
);
}
async deleteUser(userId: string) {
@@ -174,16 +198,23 @@ export class UserService extends TypeOrmQueryService<UserEntity> {
userWorkspace: UserWorkspaceEntity,
) {
const workspaceId = userWorkspace.workspaceId;
const authContext = buildSystemAuthContext(workspaceId);
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
const workspaceMembers =
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
return workspaceMemberRepository.find();
},
);
const workspaceMembers = await workspaceMemberRepository.find();
const userWorkspaceId = userWorkspace.id;
if (workspaceMembers.length === 1) {
@@ -224,7 +255,21 @@ export class UserService extends TypeOrmQueryService<UserEntity> {
assert(workspaceMember, 'WorkspaceMember not found');
await workspaceMemberRepository.delete({ userId: userWorkspace.userId });
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
await workspaceMemberRepository.delete({
userId: userWorkspace.userId,
});
},
);
await this.userWorkspaceService.deleteUserWorkspace({
userWorkspaceId,
@@ -14,10 +14,10 @@ import crypto from 'crypto';
import { msg } from '@lingui/core/macro';
import { GraphQLJSONObject } from 'graphql-type-json';
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
import { PermissionFlagType } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { In, Repository } from 'typeorm';
import { PermissionFlagType } from 'twenty-shared/constants';
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
import { SupportDriver } from 'src/engine/core-modules/twenty-config/interfaces/support.interface';
@@ -73,7 +73,8 @@ import { type UserWorkspacePermissions } from 'src/engine/metadata-modules/permi
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
import { fromUserWorkspacePermissionsToUserWorkspacePermissionsDto } from 'src/engine/metadata-modules/role/utils/fromUserWorkspacePermissionsToUserWorkspacePermissionsDto';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.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 { 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';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
@@ -103,7 +104,7 @@ export class UserResolver {
private readonly permissionsService: PermissionsService,
private readonly workspaceMemberTranspiler: WorkspaceMemberTranspiler,
private readonly userWorkspaceService: UserWorkspaceService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
) {}
private async getUserWorkspacePermissions({
@@ -425,18 +426,26 @@ export class UserResolver {
);
}
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspace.id);
const workspaceMemberToDelete = await workspaceMemberRepository.findOne({
where: {
id: workspaceMemberIdToDelete,
},
});
const workspaceMemberToDelete =
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
return workspaceMemberRepository.findOne({
where: {
id: workspaceMemberIdToDelete,
},
});
},
);
if (!isDefined(workspaceMemberToDelete)) {
throw new BadRequestException(
@@ -9,14 +9,15 @@ import {
} from '@nestjs/common';
import { Request } from 'express';
import { isDefined } from 'twenty-shared/utils';
import { FieldActorSource } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { WorkflowTriggerRestApiExceptionFilter } from 'src/engine/core-modules/workflow/filters/workflow-trigger-rest-api-exception.filter';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.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 {
WorkflowVersionStatus,
type WorkflowVersionWorkspaceEntity,
@@ -36,7 +37,7 @@ import { WorkflowTriggerWorkspaceService } from 'src/modules/workflow/workflow-t
)
export class WorkflowTriggerController {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly workflowTriggerWorkspaceService: WorkflowTriggerWorkspaceService,
) {}
@@ -72,68 +73,78 @@ export class WorkflowTriggerController {
payload?: object;
workspaceId: string;
}) {
const workflowRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowWorkspaceEntity>(
workspaceId,
'workflow',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspaceId);
const workflow = await workflowRepository.findOne({
where: { id: workflowId },
});
const { workflow } =
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workflowRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkflowWorkspaceEntity>(
workspaceId,
'workflow',
{ shouldBypassPermissionChecks: true },
);
if (!isDefined(workflow)) {
throw new WorkflowTriggerException(
`[Webhook trigger] Workflow ${workflowId} not found in workspace ${workspaceId}`,
WorkflowTriggerExceptionCode.NOT_FOUND,
);
}
const workflow = await workflowRepository.findOne({
where: { id: workflowId },
});
if (
!isDefined(workflow.lastPublishedVersionId) ||
workflow.lastPublishedVersionId === ''
) {
throw new WorkflowTriggerException(
`[Webhook trigger] Workflow ${workflowId} has not been activated in workspace ${workspaceId}`,
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_STATUS,
);
}
if (!isDefined(workflow)) {
throw new WorkflowTriggerException(
`[Webhook trigger] Workflow ${workflowId} not found in workspace ${workspaceId}`,
WorkflowTriggerExceptionCode.NOT_FOUND,
);
}
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
const workflowVersion = await workflowVersionRepository.findOne({
where: { id: workflow.lastPublishedVersionId },
});
if (
!isDefined(workflow.lastPublishedVersionId) ||
workflow.lastPublishedVersionId === ''
) {
throw new WorkflowTriggerException(
`[Webhook trigger] Workflow ${workflowId} has not been activated in workspace ${workspaceId}`,
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_STATUS,
);
}
if (!isDefined(workflowVersion)) {
throw new WorkflowTriggerException(
`[Webhook trigger] No workflow version activated for workflow ${workflowId} in workspace ${workspaceId}`,
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION,
);
}
const workflowVersionRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
const workflowVersion = await workflowVersionRepository.findOne({
where: { id: workflow.lastPublishedVersionId },
});
if (workflowVersion.trigger?.type !== WorkflowTriggerType.WEBHOOK) {
throw new WorkflowTriggerException(
`[Webhook trigger] Workflow ${workflowId} does not have a Webhook trigger in workspace ${workspaceId}`,
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
);
}
if (!isDefined(workflowVersion)) {
throw new WorkflowTriggerException(
`[Webhook trigger] No workflow version activated for workflow ${workflowId} in workspace ${workspaceId}`,
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION,
);
}
if (workflowVersion.status !== WorkflowVersionStatus.ACTIVE) {
throw new WorkflowTriggerException(
`[Webhook trigger] Workflow version ${workflowVersion.id} is not active in workspace ${workspaceId}`,
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_STATUS,
if (workflowVersion.trigger?.type !== WorkflowTriggerType.WEBHOOK) {
throw new WorkflowTriggerException(
`[Webhook trigger] Workflow ${workflowId} does not have a Webhook trigger in workspace ${workspaceId}`,
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
);
}
if (workflowVersion.status !== WorkflowVersionStatus.ACTIVE) {
throw new WorkflowTriggerException(
`[Webhook trigger] Workflow version ${workflowVersion.id} is not active in workspace ${workspaceId}`,
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_STATUS,
);
}
return { workflow, workflowVersion };
},
);
}
const { workflowRunId } =
await this.workflowTriggerWorkspaceService.runWorkflowVersion({
workflowVersionId: workflow.lastPublishedVersionId,
workflowVersionId: workflow.lastPublishedVersionId!,
payload: payload || {},
createdBy: {
source: FieldActorSource.WEBHOOK,
@@ -19,7 +19,8 @@ import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.g
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.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 { WorkflowTriggerWorkspaceService } from 'src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@@ -37,7 +38,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
)
export class WorkflowTriggerResolver {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly workflowTriggerWorkspaceService: WorkflowTriggerWorkspaceService,
) {}
@@ -72,18 +73,26 @@ export class WorkflowTriggerResolver {
@Args('input')
{ workflowVersionId, workflowRunId, payload }: RunWorkflowVersionInput,
) {
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspace.id);
const workspaceMember = await workspaceMemberRepository.findOneOrFail({
where: {
userId: user.id,
},
});
const workspaceMember =
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
return workspaceMemberRepository.findOneOrFail({
where: {
userId: user.id,
},
});
},
);
return this.workflowTriggerWorkspaceService.runWorkflowVersion({
workflowVersionId,
@@ -16,7 +16,8 @@ import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.g
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.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 { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { SendInvitationsInput } from './dtos/send-invitations.input';
@@ -33,7 +34,7 @@ import { SendInvitationsInput } from './dtos/send-invitations.input';
@Resolver()
export class WorkspaceInvitationResolver {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly workspaceInvitationService: WorkspaceInvitationService,
) {}
@@ -55,18 +56,26 @@ export class WorkspaceInvitationResolver {
@AuthWorkspace() workspace: WorkspaceEntity,
@AuthUser() user: UserEntity,
) {
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspace.id);
const workspaceMember = await workspaceMemberRepository.findOneOrFail({
where: {
userId: user.id,
},
});
const workspaceMember =
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
return workspaceMemberRepository.findOneOrFail({
where: {
userId: user.id,
},
});
},
);
return this.workspaceInvitationService.resendWorkspaceInvitation(
appTokenId,
@@ -87,18 +96,26 @@ export class WorkspaceInvitationResolver {
@AuthUser() user: UserEntity,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<SendInvitationsOutput> {
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const authContext = buildSystemAuthContext(workspace.id);
const workspaceMember = await workspaceMemberRepository.findOneOrFail({
where: {
userId: user.id,
},
});
const workspaceMember =
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
return workspaceMemberRepository.findOneOrFail({
where: {
userId: user.id,
},
});
},
);
return await this.workspaceInvitationService.sendInvitations(
sendInviteLinkInput.emails,