Refactor global datasource part 2 (#16399)

## Context
Deprecating TwentyORMManager in favor of TwentyORMGlobalManager
(temporarily, as this will simplify the ultimate goal to later replace
all usages with the new TwentyORMGlobalManagerV2 which will have a
similar signature)
This means this PR had to refactor a bit of code to pass down the
workspaceId when not available directly as it is now a requirement,
meaning we also deprecated scopedWorkspaceContextFactory to have a less
obscure way to fetch the workspaceId and have something more
declarative.

Step 3 will be to update TwentyORMGlobalManager to use a featureFlag
toggling and use the new GlobalWorkspaceOrmManager internally using the
new cache service

Step 4 will be to remove the feature flag and pg_pool patch
This commit is contained in:
Weiko
2025-12-08 17:05:00 +01:00
committed by GitHub
parent c2d3fbcd21
commit 859004f4fc
101 changed files with 647 additions and 597 deletions
@@ -7,7 +7,7 @@ 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 { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
export type UpdateSubscriptionQuantityJobData = { workspaceId: string };
@@ -21,13 +21,14 @@ export class UpdateSubscriptionQuantityJob {
constructor(
private readonly billingSubscriptionService: BillingSubscriptionService,
private readonly stripeSubscriptionItemService: StripeSubscriptionItemService,
private readonly twentyORMManager: TwentyORMManager,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
@Process(UpdateSubscriptionQuantityJob.name)
async handle(data: UpdateSubscriptionQuantityJobData): Promise<void> {
const workspaceMemberRepository =
await this.twentyORMManager.getRepository<WorkspaceMemberWorkspaceEntity>(
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
data.workspaceId,
'workspaceMember',
);
@@ -7,9 +7,11 @@ import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/
import { TIMELINE_CALENDAR_EVENTS_MAX_PAGE_SIZE } from 'src/engine/core-modules/calendar/constants/calendar.constants';
import { TimelineCalendarEventsWithTotalDTO } from 'src/engine/core-modules/calendar/dtos/timeline-calendar-events-with-total.dto';
import { TimelineCalendarEventService } from 'src/engine/core-modules/calendar/timeline-calendar-event.service';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { AuthWorkspaceMemberId } from 'src/engine/decorators/auth/auth-workspace-member-id.decorator';
import { CustomPermissionGuard } from 'src/engine/guards/custom-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
@ArgsType()
class GetTimelineCalendarEventsFromPersonIdArgs {
@@ -62,11 +64,13 @@ export class TimelineCalendarEventResolver {
@Args()
{ personId, page, pageSize }: GetTimelineCalendarEventsFromPersonIdArgs,
@AuthWorkspaceMemberId() workspaceMemberId: string,
@AuthWorkspace() workspace: WorkspaceEntity,
) {
const timelineCalendarEvents =
await this.timelineCalendarEventService.getCalendarEventsFromPersonIds({
currentWorkspaceMemberId: workspaceMemberId,
personIds: [personId],
workspaceId: workspace.id,
page,
pageSize,
});
@@ -79,11 +83,13 @@ export class TimelineCalendarEventResolver {
@Args()
{ companyId, page, pageSize }: GetTimelineCalendarEventsFromCompanyIdArgs,
@AuthWorkspaceMemberId() workspaceMemberId: string,
@AuthWorkspace() workspace: WorkspaceEntity,
) {
const timelineCalendarEvents =
await this.timelineCalendarEventService.getCalendarEventsFromCompanyId({
currentWorkspaceMemberId: workspaceMemberId,
companyId,
workspaceId: workspace.id,
page,
pageSize,
});
@@ -100,12 +106,14 @@ export class TimelineCalendarEventResolver {
pageSize,
}: GetTimelineCalendarEventsFromOpportunityIdArgs,
@AuthWorkspaceMemberId() workspaceMemberId: string,
@AuthWorkspace() workspace: WorkspaceEntity,
) {
const timelineCalendarEvents =
await this.timelineCalendarEventService.getCalendarEventsFromOpportunityId(
{
currentWorkspaceMemberId: workspaceMemberId,
opportunityId,
workspaceId: workspace.id,
page,
pageSize,
},
@@ -3,7 +3,7 @@ import { Test, type TestingModule } from '@nestjs/testing';
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
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,16 +36,18 @@ describe('TimelineCalendarEventService', () => {
findAndCount: jest.fn(),
};
const mockTwentyORMManager = {
getRepository: jest.fn().mockResolvedValue(mockCalendarEventRepository),
const mockTwentyORMGlobalManager = {
getRepositoryForWorkspace: jest
.fn()
.mockResolvedValue(mockCalendarEventRepository),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
TimelineCalendarEventService,
{
provide: TwentyORMManager,
useValue: mockTwentyORMManager,
provide: TwentyORMGlobalManager,
useValue: mockTwentyORMGlobalManager,
},
],
}).compile();
@@ -84,6 +86,7 @@ describe('TimelineCalendarEventService', () => {
const result = await service.getCalendarEventsFromPersonIds({
currentWorkspaceMemberId,
personIds,
workspaceId: 'test-workspace-id',
page: 1,
pageSize: 10,
});
@@ -123,6 +126,7 @@ describe('TimelineCalendarEventService', () => {
const result = await service.getCalendarEventsFromPersonIds({
currentWorkspaceMemberId,
personIds,
workspaceId: 'test-workspace-id',
page: 1,
pageSize: 10,
});
@@ -164,6 +168,7 @@ describe('TimelineCalendarEventService', () => {
const result = await service.getCalendarEventsFromPersonIds({
currentWorkspaceMemberId,
personIds,
workspaceId: 'test-workspace-id',
page: 1,
pageSize: 10,
});
@@ -6,7 +6,7 @@ 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 { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
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';
import { type OpportunityWorkspaceEntity } from 'src/modules/opportunity/standard-objects/opportunity.workspace-entity';
@@ -14,24 +14,29 @@ import { type PersonWorkspaceEntity } from 'src/modules/person/standard-objects/
@Injectable()
export class TimelineCalendarEventService {
constructor(private readonly twentyORMManager: TwentyORMManager) {}
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
// TODO: Align return type with the entities to avoid mapping
async getCalendarEventsFromPersonIds({
currentWorkspaceMemberId,
personIds,
workspaceId,
page = 1,
pageSize = TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE,
}: {
currentWorkspaceMemberId: string;
personIds: string[];
workspaceId: string;
page: number;
pageSize: number;
}): Promise<TimelineCalendarEventsWithTotalDTO> {
const offset = (page - 1) * pageSize;
const calendarEventRepository =
await this.twentyORMManager.getRepository<CalendarEventWorkspaceEntity>(
await this.twentyORMGlobalManager.getRepositoryForWorkspace<CalendarEventWorkspaceEntity>(
workspaceId,
'calendarEvent',
);
@@ -162,16 +167,19 @@ export class TimelineCalendarEventService {
async getCalendarEventsFromCompanyId({
currentWorkspaceMemberId,
companyId,
workspaceId,
page = 1,
pageSize = TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE,
}: {
currentWorkspaceMemberId: string;
companyId: string;
workspaceId: string;
page: number;
pageSize: number;
}): Promise<TimelineCalendarEventsWithTotalDTO> {
const personRepository =
await this.twentyORMManager.getRepository<PersonWorkspaceEntity>(
await this.twentyORMGlobalManager.getRepositoryForWorkspace<PersonWorkspaceEntity>(
workspaceId,
'person',
);
@@ -196,6 +204,7 @@ export class TimelineCalendarEventService {
const calendarEvents = await this.getCalendarEventsFromPersonIds({
currentWorkspaceMemberId,
personIds: formattedPersonIds,
workspaceId,
page,
pageSize,
});
@@ -206,16 +215,19 @@ export class TimelineCalendarEventService {
async getCalendarEventsFromOpportunityId({
currentWorkspaceMemberId,
opportunityId,
workspaceId,
page = 1,
pageSize = TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE,
}: {
currentWorkspaceMemberId: string;
opportunityId: string;
workspaceId: string;
page: number;
pageSize: number;
}): Promise<TimelineCalendarEventsWithTotalDTO> {
const opportunityRepository =
await this.twentyORMManager.getRepository<OpportunityWorkspaceEntity>(
await this.twentyORMGlobalManager.getRepositoryForWorkspace<OpportunityWorkspaceEntity>(
workspaceId,
'opportunity',
);
@@ -238,6 +250,7 @@ export class TimelineCalendarEventService {
const calendarEvents = await this.getCalendarEventsFromCompanyId({
currentWorkspaceMemberId,
companyId: opportunity.companyId,
workspaceId,
page,
pageSize,
});
@@ -37,7 +37,6 @@ import { MessageQueueModule } from 'src/engine/core-modules/message-queue/messag
import { messageQueueModuleFactory } from 'src/engine/core-modules/message-queue/message-queue.module-factory';
import { TimelineMessagingModule } from 'src/engine/core-modules/messaging/timeline-messaging.module';
import { OpenApiModule } from 'src/engine/core-modules/open-api/open-api.module';
import { PageLayoutModule } from 'src/engine/metadata-modules/page-layout/page-layout.module';
import { PostgresCredentialsModule } from 'src/engine/core-modules/postgres-credentials/postgres-credentials.module';
import { PublicDomainModule } from 'src/engine/core-modules/public-domain/public-domain.module';
import { RedisClientModule } from 'src/engine/core-modules/redis-client/redis-client.module';
@@ -56,12 +55,13 @@ import { WorkspaceInvitationModule } from 'src/engine/core-modules/workspace-inv
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
import { AiBillingModule } from 'src/engine/metadata-modules/ai/ai-billing/ai-billing.module';
import { AiModelsModule } from 'src/engine/metadata-modules/ai/ai-models/ai-models.module';
import { FlatPageLayoutTabModule } from 'src/engine/metadata-modules/flat-page-layout-tab/flat-page-layout-tab.module';
import { RoleModule } from 'src/engine/metadata-modules/role/role.module';
import { SubscriptionsModule } from 'src/engine/subscriptions/subscriptions.module';
import { TrashCleanupModule } from 'src/engine/trash-cleanup/trash-cleanup.module';
import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module';
import { ChannelSyncModule } from 'src/modules/connected-account/channel-sync/channel-sync.module';
import { FlatPageLayoutTabModule } from 'src/engine/metadata-modules/flat-page-layout-tab/flat-page-layout-tab.module';
import { PageLayoutModule } from 'src/engine/metadata-modules/page-layout/page-layout.module';
import { AuditModule } from './audit/audit.module';
import { ClientConfigModule } from './client-config/client-config.module';
@@ -4,20 +4,21 @@ 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 { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
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 twentyORMManager: TwentyORMManager,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly timelineMessagingService: TimelineMessagingService,
) {}
async getMessagesFromPersonIds(
workspaceMemberId: string,
personIds: string[],
workspaceId: string,
page = 1,
pageSize: number = TIMELINE_THREADS_DEFAULT_PAGE_SIZE,
): Promise<TimelineThreadsWithTotalDTO> {
@@ -26,6 +27,7 @@ export class GetMessagesService {
const { messageThreads, totalNumberOfThreads } =
await this.timelineMessagingService.getAndCountMessageThreads(
personIds,
workspaceId,
offset,
pageSize,
);
@@ -44,12 +46,14 @@ export class GetMessagesService {
const threadParticipantsByThreadId =
await this.timelineMessagingService.getThreadParticipantsByThreadId(
messageThreadIds,
workspaceId,
);
const threadVisibilityByThreadId =
await this.timelineMessagingService.getThreadVisibilityByThreadId(
messageThreadIds,
workspaceMemberId,
workspaceId,
);
return {
@@ -65,11 +69,13 @@ export class GetMessagesService {
async getMessagesFromCompanyId(
workspaceMemberId: string,
companyId: string,
workspaceId: string,
page = 1,
pageSize: number = TIMELINE_THREADS_DEFAULT_PAGE_SIZE,
): Promise<TimelineThreadsWithTotalDTO> {
const personRepository =
await this.twentyORMManager.getRepository<PersonWorkspaceEntity>(
await this.twentyORMGlobalManager.getRepositoryForWorkspace<PersonWorkspaceEntity>(
workspaceId,
'person',
);
const personIds = (
@@ -93,6 +99,7 @@ export class GetMessagesService {
const messageThreads = await this.getMessagesFromPersonIds(
workspaceMemberId,
personIds,
workspaceId,
page,
pageSize,
);
@@ -103,11 +110,13 @@ export class GetMessagesService {
async getMessagesFromOpportunityId(
workspaceMemberId: string,
opportunityId: string,
workspaceId: string,
page = 1,
pageSize: number = TIMELINE_THREADS_DEFAULT_PAGE_SIZE,
): Promise<TimelineThreadsWithTotalDTO> {
const opportunityRepository =
await this.twentyORMManager.getRepository<OpportunityWorkspaceEntity>(
await this.twentyORMGlobalManager.getRepositoryForWorkspace<OpportunityWorkspaceEntity>(
workspaceId,
'opportunity',
);
@@ -130,6 +139,7 @@ export class GetMessagesService {
const messageThreads = await this.getMessagesFromCompanyId(
workspaceMemberId,
opportunity.companyId,
workspaceId,
page,
pageSize,
);
@@ -3,17 +3,20 @@ import { Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import { type TimelineThreadDTO } from 'src/engine/core-modules/messaging/dtos/timeline-thread.dto';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
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';
@Injectable()
export class TimelineMessagingService {
constructor(private readonly twentyORMManager: TwentyORMManager) {}
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
public async getAndCountMessageThreads(
personIds: string[],
workspaceId: string,
offset: number,
pageSize: number,
): Promise<{
@@ -28,7 +31,8 @@ export class TimelineMessagingService {
totalNumberOfThreads: number;
}> {
const messageThreadRepository =
await this.twentyORMManager.getRepository<MessageThreadWorkspaceEntity>(
await this.twentyORMGlobalManager.getRepositoryForWorkspace<MessageThreadWorkspaceEntity>(
workspaceId,
'messageThread',
);
@@ -87,11 +91,13 @@ export class TimelineMessagingService {
public async getThreadParticipantsByThreadId(
messageThreadIds: string[],
workspaceId: string,
): Promise<{
[key: string]: MessageParticipantWorkspaceEntity[];
}> {
const messageParticipantRepository =
await this.twentyORMManager.getRepository<MessageParticipantWorkspaceEntity>(
await this.twentyORMGlobalManager.getRepositoryForWorkspace<MessageParticipantWorkspaceEntity>(
workspaceId,
'messageParticipant',
);
@@ -176,11 +182,13 @@ export class TimelineMessagingService {
public async getThreadVisibilityByThreadId(
messageThreadIds: string[],
workspaceMemberId: string,
workspaceId: string,
): Promise<{
[key: string]: MessageChannelVisibility;
}> {
const messageThreadRepository =
await this.twentyORMManager.getRepository<MessageThreadWorkspaceEntity>(
await this.twentyORMGlobalManager.getRepositoryForWorkspace<MessageThreadWorkspaceEntity>(
workspaceId,
'messageThread',
);
@@ -93,6 +93,7 @@ export class TimelineMessagingResolver {
await this.getMessagesFromPersonIdsService.getMessagesFromPersonIds(
workspaceMember.id,
[personId],
workspace.id,
page,
pageSize,
);
@@ -119,6 +120,7 @@ export class TimelineMessagingResolver {
await this.getMessagesFromPersonIdsService.getMessagesFromCompanyId(
workspaceMember.id,
companyId,
workspace.id,
page,
pageSize,
);
@@ -146,6 +148,7 @@ export class TimelineMessagingResolver {
await this.getMessagesFromPersonIdsService.getMessagesFromOpportunityId(
workspaceMember.id,
opportunityId,
workspace.id,
page,
pageSize,
);
@@ -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 { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.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: TwentyORMManager, useValue: {} },
{ provide: TwentyORMGlobalManager, useValue: {} },
{ provide: WorkspaceCacheStorageService, useValue: {} },
{ provide: FileService, useValue: {} },
],
@@ -4,9 +4,16 @@ import { FileModule } from 'src/engine/core-modules/file/file.module';
import { SearchResolver } from 'src/engine/core-modules/search/search.resolver';
import { SearchService } from 'src/engine/core-modules/search/services/search.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
@Module({
imports: [FileModule, WorkspaceManyOrAllFlatEntityMapsCacheModule],
imports: [
FileModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
UserRoleModule,
PermissionsModule,
],
providers: [SearchResolver, SearchService],
})
export class SearchModule {}
@@ -3,6 +3,7 @@ import { Args, Query, Resolver } from '@nestjs/graphql';
import { isDefined } from 'twenty-shared/utils';
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-key-role.service';
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
import { SearchArgs } from 'src/engine/core-modules/search/dtos/search-args';
@@ -10,10 +11,14 @@ import { SearchResultConnectionDTO } from 'src/engine/core-modules/search/dtos/s
import { SearchApiExceptionFilter } from 'src/engine/core-modules/search/filters/search-api-exception.filter';
import { SearchService } from 'src/engine/core-modules/search/services/search.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthApiKey } from 'src/engine/decorators/auth/auth-api-key.decorator';
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { CustomPermissionGuard } from 'src/engine/guards/custom-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
@Resolver()
@UseFilters(SearchApiExceptionFilter, PreventNestToAutoLogGraphqlErrorsFilter)
@@ -23,11 +28,15 @@ export class SearchResolver {
constructor(
private readonly searchService: SearchService,
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly userRoleService: UserRoleService,
private readonly apiKeyRoleService: ApiKeyRoleService,
) {}
@Query(() => SearchResultConnectionDTO)
async search(
@AuthWorkspace() workspace: WorkspaceEntity,
@AuthUserWorkspaceId() userWorkspaceId: string | undefined,
@AuthApiKey() apiKey: string | undefined,
@Args()
{
searchInput,
@@ -57,6 +66,29 @@ export class SearchResolver {
excludedObjectNameSingulars: excludedObjectNameSingulars ?? [],
});
// TODO: move to a service
let rolePermissionConfig: RolePermissionConfig | undefined;
if (isDefined(apiKey)) {
const roleId = await this.apiKeyRoleService.getRoleIdForApiKey(
apiKey,
workspace.id,
);
if (isDefined(roleId)) {
rolePermissionConfig = { unionOf: [roleId] };
}
} else if (isDefined(userWorkspaceId)) {
const roleId = await this.userRoleService.getRoleIdForUserWorkspace({
userWorkspaceId,
workspaceId: workspace.id,
});
if (isDefined(roleId)) {
rolePermissionConfig = { unionOf: [roleId] };
}
}
const allRecordsWithObjectMetadataItems =
await this.searchService.getAllRecordsWithObjectMetadataItems({
flatObjectMetadatas: filteredObjectMetadataItems,
@@ -67,6 +99,8 @@ export class SearchResolver {
includedObjectNameSingulars,
excludedObjectNameSingulars,
after,
workspaceId: workspace.id,
rolePermissionConfig,
});
return this.searchService.computeSearchObjectResults({
@@ -31,7 +31,8 @@ import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-m
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 { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
type LastRanks = { tsRankCD: number; tsRank: number };
@@ -45,7 +46,7 @@ const OBJECT_METADATA_ITEMS_CHUNK_SIZE = 5;
@Injectable()
export class SearchService {
constructor(
private readonly twentyORMManager: TwentyORMManager,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly fileService: FileService,
) {}
@@ -58,9 +59,13 @@ export class SearchService {
limit,
filter,
after,
workspaceId,
rolePermissionConfig,
}: {
flatObjectMetadatas: FlatObjectMetadata[];
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
workspaceId: string;
rolePermissionConfig?: RolePermissionConfig;
} & SearchArgs) {
const filteredObjectMetadataItems = this.filterObjectMetadataItems({
flatObjectMetadatas,
@@ -80,8 +85,10 @@ export class SearchService {
const recordsWithObjectMetadataItems = await Promise.all(
objectMetadataItemChunk.map(async (flatObjectMetadata) => {
const repository =
await this.twentyORMManager.getRepository<ObjectRecord>(
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ObjectRecord>(
workspaceId,
flatObjectMetadata.nameSingular,
rolePermissionConfig,
);
return {
@@ -173,7 +173,7 @@ export class ToolProviderService {
description: tool.description,
inputSchema: tool.inputSchema,
execute: async (parameters: { input: ToolInput }) =>
tool.execute(parameters.input),
tool.execute(parameters.input, spec.workspaceId),
};
} else if (spec.rolePermissionConfig && spec.workspaceId) {
const hasPermission = await this.permissionsService.hasToolPermission(
@@ -187,7 +187,7 @@ export class ToolProviderService {
description: tool.description,
inputSchema: tool.inputSchema,
execute: async (parameters: { input: ToolInput }) =>
tool.execute(parameters.input),
tool.execute(parameters.input, spec.workspaceId),
};
}
}
@@ -26,7 +26,8 @@ export class ToolRegistryService {
return {
description: httpTool.description,
inputSchema: httpTool.inputSchema,
execute: (params) => httpTool.execute(params),
execute: (params, workspaceId) =>
httpTool.execute(params, workspaceId),
flag: PermissionFlagType.HTTP_REQUEST_TOOL,
};
},
@@ -36,8 +37,8 @@ export class ToolRegistryService {
() => ({
description: this.sendEmailTool.description,
inputSchema: this.sendEmailTool.inputSchema,
execute: (params) =>
this.sendEmailTool.execute(params as SendEmailInput),
execute: (params, workspaceId) =>
this.sendEmailTool.execute(params as SendEmailInput, workspaceId),
flag: PermissionFlagType.SEND_EMAIL_TOOL,
}),
],
@@ -20,7 +20,10 @@ export class HttpTool implements Tool {
constructor(private readonly twentyConfigService: TwentyConfigService) {}
async execute(parameters: ToolInput): Promise<ToolOutput> {
async execute(
parameters: ToolInput,
_workspaceId: string,
): Promise<ToolOutput> {
const { url, method, headers, body } = parameters as HttpRequestInput;
const headersCopy = { ...headers };
const isMethodForBody = ['POST', 'PUT', 'PATCH'].includes(method);
@@ -18,7 +18,6 @@ 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 { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
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';
@@ -36,7 +35,6 @@ export class SendEmailTool implements Tool {
inputSchema = SendEmailToolParametersZodSchema;
constructor(
private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly sendMessageService: MessagingSendMessageService,
@InjectRepository(FileEntity)
@@ -160,9 +158,10 @@ export class SendEmailTool implements Tool {
return attachments;
}
async execute(parameters: SendEmailInput): Promise<ToolOutput> {
const { workspaceId } = this.scopedWorkspaceContextFactory.create();
async execute(
parameters: SendEmailInput,
workspaceId: string,
): Promise<ToolOutput> {
const { email, subject, body, files } = parameters;
let { connectedAccountId } = parameters;
@@ -180,13 +179,6 @@ export class SendEmailTool implements Tool {
);
}
if (!workspaceId) {
throw new SendEmailToolException(
'Workspace ID not found',
SendEmailToolExceptionCode.WORKSPACE_ID_NOT_FOUND,
);
}
if (!connectedAccountId) {
connectedAccountId =
await this.getOrThrowFirstConnectedAccountId(workspaceId);
@@ -7,6 +7,6 @@ import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.
export type Tool = {
description: string;
inputSchema: FlexibleSchema<unknown>;
execute(input: ToolInput): Promise<ToolOutput>;
execute(input: ToolInput, workspaceId: string): Promise<ToolOutput>;
flag?: PermissionFlagType;
};
@@ -141,6 +141,7 @@ export class WorkflowTriggerController {
name: 'Webhook',
context: {},
},
workspaceId,
});
return {
@@ -43,21 +43,25 @@ export class WorkflowTriggerResolver {
@Mutation(() => Boolean)
async activateWorkflowVersion(
@AuthWorkspace() workspace: WorkspaceEntity,
@Args('workflowVersionId', { type: () => UUIDScalarType })
workflowVersionId: string,
) {
return this.workflowTriggerWorkspaceService.activateWorkflowVersion(
workflowVersionId,
workspace.id,
);
}
@Mutation(() => Boolean)
async deactivateWorkflowVersion(
@AuthWorkspace() workspace: WorkspaceEntity,
@Args('workflowVersionId', { type: () => UUIDScalarType })
workflowVersionId: string,
) {
return this.workflowTriggerWorkspaceService.deactivateWorkflowVersion(
workflowVersionId,
workspace.id,
);
}
@@ -92,14 +96,19 @@ export class WorkflowTriggerResolver {
},
workspaceMemberId: workspaceMember.id,
}),
workspaceId: workspace.id,
});
}
@Mutation(() => WorkflowRunDTO)
async stopWorkflowRun(
@AuthWorkspace() workspace: WorkspaceEntity,
@Args('workflowRunId', { type: () => UUIDScalarType })
workflowRunId: string,
) {
return this.workflowTriggerWorkspaceService.stopWorkflowRun(workflowRunId);
return this.workflowTriggerWorkspaceService.stopWorkflowRun(
workflowRunId,
workspace.id,
);
}
}
@@ -150,14 +150,18 @@ export class WorkflowVersionStepResolver {
@Mutation(() => TestHttpRequestOutput)
async testHttpRequest(
@AuthWorkspace() workspace: WorkspaceEntity,
@Args('input')
{ url, method, headers, body }: TestHttpRequestInput,
): Promise<TestHttpRequestOutput> {
return this.httpTool.execute({
url,
method,
headers,
body,
});
return this.httpTool.execute(
{
url,
method,
headers,
body,
},
workspace.id,
);
}
}