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
@@ -9,7 +9,8 @@ import {
AgentExceptionCode,
} from 'src/engine/metadata-modules/ai/ai-agent/agent.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';
export type AgentActorContext = {
actorContext: ActorMetadata;
@@ -22,13 +23,15 @@ export class AgentActorContextService {
constructor(
private readonly userWorkspaceService: UserWorkspaceService,
private readonly userRoleService: UserRoleService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
) {}
async buildUserAndAgentActorContext(
userWorkspaceId: string,
workspaceId: string,
): Promise<AgentActorContext> {
const authContext = buildSystemAuthContext(workspaceId);
const userWorkspace =
await this.userWorkspaceService.findById(userWorkspaceId);
@@ -39,18 +42,24 @@ export class AgentActorContextService {
);
}
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const workspaceMember =
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const workspaceMember = await workspaceMemberRepository.findOne({
where: {
userId: userWorkspace.userId,
},
});
return workspaceMemberRepository.findOne({
where: {
userId: userWorkspace.userId,
},
});
},
);
if (!workspaceMember) {
throw new AgentException(
@@ -44,8 +44,9 @@ import { CHAT_SYSTEM_PROMPTS } from 'src/engine/metadata-modules/ai/ai-chat/cons
import { ModelProvider } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
import { AI_TELEMETRY_CONFIG } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-telemetry.const';
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { getWorkspaceContext } from 'src/engine/twenty-orm/storage/orm-workspace-context.storage';
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
export type ChatExecutionOptions = {
workspace: WorkspaceEntity;
@@ -75,8 +76,7 @@ export class ChatExecutionService {
private readonly aiModelRegistryService: AiModelRegistryService,
private readonly aiBillingService: AIBillingService,
private readonly agentActorContextService: AgentActorContextService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly workspaceDomainsService: WorkspaceDomainsService,
) {}
@@ -212,106 +212,116 @@ export class ChatExecutionService {
recordIdsByObjectMetadataNameSingular: RecordIdsByObjectMetadataNameSingularType,
userWorkspaceId: string,
): Promise<string> {
const { userWorkspaceRoleMap } =
await this.workspaceCacheService.getOrRecompute(workspace.id, [
'userWorkspaceRoleMap',
]);
const authContext = buildSystemAuthContext(workspace.id);
const roleId = userWorkspaceRoleMap[userWorkspaceId];
const contextFromRecords =
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
permissionsPerRoleId: objectMetadataPermissions,
userWorkspaceRoleMap,
} = getWorkspaceContext();
if (!roleId) {
throw new AgentException(
'Failed to retrieve user role.',
AgentExceptionCode.ROLE_NOT_FOUND,
);
}
const roleId = userWorkspaceRoleMap[userWorkspaceId];
const workspaceDataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
workspaceId: workspace.id,
});
if (!roleId) {
throw new AgentException(
'Failed to retrieve user role.',
AgentExceptionCode.ROLE_NOT_FOUND,
);
}
const flatObjectMetadataMaps =
workspaceDataSource.internalContext.flatObjectMetadataMaps;
const flatFieldMetadataMaps =
workspaceDataSource.internalContext.flatFieldMetadataMaps;
const objectIdByNameSingular =
workspaceDataSource.internalContext.objectIdByNameSingular;
const objectMetadataPermissions = workspaceDataSource.permissionsPerRoleId;
const contextObject = (
await Promise.all(
recordIdsByObjectMetadataNameSingular.map(
async (recordsWithObjectMetadataNameSingular) => {
if (recordsWithObjectMetadataNameSingular.recordIds.length === 0) {
return [];
}
const objectMetadataId =
objectIdByNameSingular[
recordsWithObjectMetadataNameSingular.objectMetadataNameSingular
];
const objectMetadataMapItem = objectMetadataId
? flatObjectMetadataMaps.byId[objectMetadataId]
: undefined;
if (!objectMetadataMapItem) {
this.logger.warn(
`Object metadata not found for ${recordsWithObjectMetadataNameSingular.objectMetadataNameSingular}`,
);
return [];
}
const repository = workspaceDataSource.getRepository(
recordsWithObjectMetadataNameSingular.objectMetadataNameSingular,
{ unionOf: [roleId] },
const workspaceDataSource =
await this.globalWorkspaceOrmManager.getDataSourceForWorkspace(
workspace.id,
);
const restrictedFields =
objectMetadataPermissions?.[roleId]?.[objectMetadataMapItem.id]
?.restrictedFields ?? {};
const contextObject = (
await Promise.all(
recordIdsByObjectMetadataNameSingular.map(
async (recordsWithObjectMetadataNameSingular) => {
if (
recordsWithObjectMetadataNameSingular.recordIds.length === 0
) {
return [];
}
const hasRestrictedFields = Object.values(restrictedFields).some(
(field) => field.canRead === false,
);
const objectMetadataId =
objectIdByNameSingular[
recordsWithObjectMetadataNameSingular
.objectMetadataNameSingular
];
const objectMetadataMapItem = objectMetadataId
? flatObjectMetadataMaps.byId[objectMetadataId]
: undefined;
const selectOptions = hasRestrictedFields
? getAllSelectableColumnNames({
restrictedFields,
objectMetadata: {
objectMetadataMapItem,
flatFieldMetadataMaps,
},
})
: undefined;
if (!objectMetadataMapItem) {
this.logger.warn(
`Object metadata not found for ${recordsWithObjectMetadataNameSingular.objectMetadataNameSingular}`,
);
return (
await repository.find({
...(selectOptions && { select: selectOptions }),
where: {
id: In(recordsWithObjectMetadataNameSingular.recordIds),
return [];
}
const repository = workspaceDataSource.getRepository(
recordsWithObjectMetadataNameSingular.objectMetadataNameSingular,
{ unionOf: [roleId] },
);
const restrictedFields =
objectMetadataPermissions?.[roleId]?.[
objectMetadataMapItem.id
]?.restrictedFields ?? {};
const hasRestrictedFields = Object.values(
restrictedFields,
).some((field) => field.canRead === false);
const selectOptions = hasRestrictedFields
? getAllSelectableColumnNames({
restrictedFields,
objectMetadata: {
objectMetadataMapItem,
flatFieldMetadataMaps,
},
})
: undefined;
return (
await repository.find({
...(selectOptions && { select: selectOptions }),
where: {
id: In(recordsWithObjectMetadataNameSingular.recordIds),
},
})
).map((record) => {
return {
...record,
resourceUrl:
this.workspaceDomainsService.buildWorkspaceURL({
workspace,
pathname: getAppPath(AppPath.RecordShowPage, {
objectNameSingular:
recordsWithObjectMetadataNameSingular.objectMetadataNameSingular,
objectRecordId: record.id,
}),
}),
};
});
},
})
).map((record) => {
return {
...record,
resourceUrl: this.workspaceDomainsService.buildWorkspaceURL({
workspace,
pathname: getAppPath(AppPath.RecordShowPage, {
objectNameSingular:
recordsWithObjectMetadataNameSingular.objectMetadataNameSingular,
objectRecordId: record.id,
}),
}),
};
});
},
),
)
).flat(2);
),
)
).flat(2);
return JSON.stringify(contextObject);
return JSON.stringify(contextObject);
},
);
return contextFromRecords;
}
private getLastUserMessage(
@@ -31,7 +31,8 @@ import {
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
import { ViewKey } from 'src/engine/metadata-modules/view/enums/view-key.enum';
import { ViewType } from 'src/engine/metadata-modules/view/enums/view-type.enum';
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 { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { DEFAULT_VIEW_FIELD_SIZE } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/constants/DEFAULT_VIEW_FIELD_SIZE';
import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
@@ -46,7 +47,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly applicationService: ApplicationService,
) {
super(objectMetadataRepository);
@@ -509,18 +510,25 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
view: FlatView;
workspaceId: string;
}) {
const favoriteRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<FavoriteWorkspaceEntity>(
workspaceId,
'favorite',
);
const authContext = buildSystemAuthContext(workspaceId);
const favoriteCount = await favoriteRepository.count();
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const favoriteRepository =
await this.globalWorkspaceOrmManager.getRepository<FavoriteWorkspaceEntity>(
workspaceId,
'favorite',
);
await favoriteRepository.insert({
viewId: view.id,
position: favoriteCount,
});
const favoriteCount = await favoriteRepository.count();
await favoriteRepository.insert({
viewId: view.id,
position: favoriteCount,
});
},
);
}
public async deleteWorkspaceAllObjectMetadata({
@@ -30,7 +30,8 @@ import {
} from 'src/engine/metadata-modules/page-layout/exceptions/page-layout.exception';
import { fromFlatPageLayoutToPageLayoutDto } from 'src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-to-page-layout-dto.util';
import { fromFlatPageLayoutWithTabsAndWidgetsToPageLayoutDto } from 'src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-with-tabs-and-widgets-to-page-layout-dto.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 { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service';
@@ -39,7 +40,7 @@ export class PageLayoutService {
private readonly logger = new Logger(PageLayoutService.name);
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly applicationService: ApplicationService,
@@ -376,23 +377,30 @@ export class PageLayoutService {
pageLayoutId: string,
workspaceId: string,
): Promise<void> {
const authContext = buildSystemAuthContext(workspaceId);
try {
const dashboardRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
'dashboard',
{ shouldBypassPermissionChecks: true },
);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const dashboardRepository =
await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
'dashboard',
{ shouldBypassPermissionChecks: true },
);
const dashboards = await dashboardRepository.find({
where: {
pageLayoutId,
const dashboards = await dashboardRepository.find({
where: {
pageLayoutId,
},
});
for (const dashboard of dashboards) {
await dashboardRepository.delete(dashboard.id);
}
},
});
for (const dashboard of dashboards) {
await dashboardRepository.delete(dashboard.id);
}
);
} catch (error) {
this.logger.error(
`Failed to destroy associated dashboards for page layout ${pageLayoutId}: ${error}`,
@@ -13,7 +13,8 @@ import {
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { RoleTargetService } from 'src/engine/metadata-modules/role-target/services/role-target.service';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.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 { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { ADMIN_ROLE } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/admin-role';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@@ -24,7 +25,7 @@ export class UserRoleService {
private readonly roleTargetRepository: Repository<RoleTargetEntity>,
@InjectRepository(UserWorkspaceEntity)
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly roleTargetService: RoleTargetService,
private readonly workspaceCacheService: WorkspaceCacheService,
) {}
@@ -137,6 +138,8 @@ export class UserRoleService {
roleId: string,
workspaceId: string,
): Promise<WorkspaceMemberWorkspaceEntity[]> {
const authContext = buildSystemAuthContext(workspaceId);
const userWorkspaceIdsWithRole =
await this.getUserWorkspaceIdsAssignedToRole(roleId, workspaceId);
@@ -150,20 +153,25 @@ export class UserRoleService {
userWorkspaces.map((userWorkspace) => userWorkspace.userId),
);
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const workspaceMembers = await workspaceMemberRepository.find({
where: {
userId: In(userIds),
const workspaceMembers = await workspaceMemberRepository.find({
where: {
userId: In(userIds),
},
});
return workspaceMembers;
},
});
return workspaceMembers;
);
}
public async getUserWorkspaceIdsAssignedToRole(