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
@@ -1,7 +1,6 @@
import { EntitySchemaColumnFactory } from 'src/engine/twenty-orm/factories/entity-schema-column.factory';
import { EntitySchemaRelationFactory } from 'src/engine/twenty-orm/factories/entity-schema-relation.factory';
import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory';
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
import { WorkspaceDatasourceFactory } from 'src/engine/twenty-orm/factories/workspace-datasource.factory';
export const entitySchemaFactories = [
@@ -9,5 +8,4 @@ export const entitySchemaFactories = [
EntitySchemaRelationFactory,
EntitySchemaFactory,
WorkspaceDatasourceFactory,
ScopedWorkspaceContextFactory,
];
@@ -1,45 +0,0 @@
import { Inject, Injectable, Optional, Scope } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
@Injectable({ scope: Scope.REQUEST })
export class ScopedWorkspaceContextFactory {
constructor(
@Optional()
@Inject(REQUEST)
private readonly request: Request | null,
) {}
public create(): {
workspaceId: string | null;
userWorkspaceId: string | null;
isExecutedByApiKey: boolean;
apiKeyId: string | null;
} {
const workspaceId: string | undefined =
// @ts-expect-error legacy noImplicitAny
this.request?.['req']?.['workspaceId'] ||
// @ts-expect-error legacy noImplicitAny
this.request?.['params']?.['workspaceId'] ||
// @ts-expect-error legacy noImplicitAny
this.request?.['workspace']?.['id']; // rest api
// Extract API key ID (same pattern as userWorkspaceId)
const apiKey =
// @ts-expect-error legacy noImplicitAny
this.request?.['req']?.['apiKey'] ||
// @ts-expect-error legacy noImplicitAny
this.request?.['apiKey']; // rest api
return {
workspaceId: workspaceId ?? null,
userWorkspaceId:
// @ts-expect-error legacy noImplicitAny
this.request?.['req']?.['userWorkspaceId'] ??
// @ts-expect-error legacy noImplicitAny
this.request?.['userWorkspaceId'] ?? // rest api
null,
isExecutedByApiKey: !!apiKey,
apiKeyId: apiKey?.id ?? null,
};
}
}
@@ -1,90 +0,0 @@
import { Injectable, type Type } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { type ObjectLiteral, Repository } from 'typeorm';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
import { WorkspaceDatasourceFactory } from 'src/engine/twenty-orm/factories/workspace-datasource.factory';
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util';
@Injectable()
export class TwentyORMManager {
constructor(
@InjectRepository(RoleTargetEntity)
private readonly roleTargetRepository: Repository<RoleTargetEntity>,
private readonly workspaceDataSourceFactory: WorkspaceDatasourceFactory,
private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory,
) {}
async getRepository<T extends ObjectLiteral>(
workspaceEntity: Type<T>,
): Promise<WorkspaceRepository<T>>;
async getRepository<T extends ObjectLiteral>(
objectMetadataName: string,
): Promise<WorkspaceRepository<T>>;
async getRepository<T extends ObjectLiteral>(
workspaceEntityOrObjectMetadataName: Type<T> | string,
): Promise<WorkspaceRepository<T>> {
const { workspaceId, userWorkspaceId, apiKeyId } =
this.scopedWorkspaceContextFactory.create();
let objectMetadataName: string;
if (typeof workspaceEntityOrObjectMetadataName === 'string') {
objectMetadataName = workspaceEntityOrObjectMetadataName;
} else {
objectMetadataName = convertClassNameToObjectMetadataName(
workspaceEntityOrObjectMetadataName.name,
);
}
if (!workspaceId) {
throw new Error('Workspace not found');
}
const workspaceDataSource =
await this.workspaceDataSourceFactory.create(workspaceId);
let roleId: string | undefined;
if (isDefined(userWorkspaceId)) {
const roleTarget = await this.roleTargetRepository.findOne({
where: {
userWorkspaceId,
workspaceId,
},
});
roleId = roleTarget?.roleId;
} else if (isDefined(apiKeyId)) {
const roleTarget = await this.roleTargetRepository.findOne({
where: {
apiKeyId,
workspaceId,
},
});
roleId = roleTarget?.roleId;
}
return workspaceDataSource.getRepository<T>(
objectMetadataName,
roleId ? { unionOf: [roleId] } : undefined,
);
}
async getDatasource() {
const { workspaceId } = this.scopedWorkspaceContextFactory.create();
if (!workspaceId) {
throw new Error('Workspace not found');
}
return this.workspaceDataSourceFactory.create(workspaceId);
}
}
@@ -12,9 +12,7 @@ import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-t
import { WorkspaceFeatureFlagsMapCacheModule } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.module';
import { entitySchemaFactories } from 'src/engine/twenty-orm/factories';
import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory';
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
@@ -38,17 +36,7 @@ import { PgPoolSharedModule } from './pg-shared-pool/pg-shared-pool.module';
PgPoolSharedModule,
WorkspaceCacheModule,
],
providers: [
...entitySchemaFactories,
TwentyORMManager,
TwentyORMGlobalManager,
],
exports: [
EntitySchemaFactory,
TwentyORMManager,
TwentyORMGlobalManager,
PgPoolSharedModule,
ScopedWorkspaceContextFactory,
],
providers: [...entitySchemaFactories, TwentyORMGlobalManager],
exports: [EntitySchemaFactory, TwentyORMGlobalManager, PgPoolSharedModule],
})
export class TwentyORMModule {}