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
@@ -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;
},
);
}
}