Refactor workspace creation (#16689)

# Introduction
Created an env variable to will inject a feature flag during any new
workspace init to be created through v2
This commit is contained in:
Paul Rastoin
2025-12-19 12:47:31 +01:00
committed by GitHub
parent 32bb69c52f
commit 6f2ff06a35
11 changed files with 368 additions and 33 deletions
@@ -16,4 +16,5 @@ export enum FeatureFlagKey {
IS_TIMELINE_ACTIVITY_MIGRATED = 'IS_TIMELINE_ACTIVITY_MIGRATED',
IS_GLOBAL_WORKSPACE_DATASOURCE_ENABLED = 'IS_GLOBAL_WORKSPACE_DATASOURCE_ENABLED',
IS_READ_ON_REPLICA_ENABLED = 'IS_READ_ON_REPLICA_ENABLED',
IS_WORKSPACE_CREATION_V2_ENABLED = 'IS_WORKSPACE_CREATION_V2_ENABLED',
}
@@ -1393,6 +1393,16 @@ export class ConfigVariables {
@IsOptional()
IS_MAPS_AND_ADDRESS_AUTOCOMPLETE_ENABLED = false;
@ConfigVariablesMetadata({
group: ConfigVariablesGroup.OTHER,
description:
'Enable or disable workspace creation through v2 workspaceMigration',
type: ConfigVariableType.BOOLEAN,
isEnvOnly: true,
})
@IsOptional()
IS_WORKSPACE_CREATION_V2_ENABLED = false;
@ConfigVariablesMetadata({
group: ConfigVariablesGroup.OTHER,
isSensitive: true,
@@ -17,6 +17,7 @@ import { DnsManagerService } from 'src/engine/core-modules/dns-manager/services/
import { CustomDomainManagerService } from 'src/engine/core-modules/domain/custom-domain-manager/services/custom-domain-manager.service';
import { SubdomainManagerService } from 'src/engine/core-modules/domain/subdomain-manager/services/subdomain-manager.service';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import {
FileWorkspaceFolderDeletionJob,
@@ -242,8 +243,17 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
activationStatus: WorkspaceActivationStatus.ONGOING_CREATION,
});
const isV2SyncEnabled = this.twentyConfigService.get(
'IS_WORKSPACE_CREATION_V2_ENABLED',
);
await this.featureFlagService.enableFeatureFlags(
DEFAULT_FEATURE_FLAGS,
[
...DEFAULT_FEATURE_FLAGS,
...(isV2SyncEnabled
? [FeatureFlagKey.IS_WORKSPACE_CREATION_V2_ENABLED]
: []),
],
workspace.id,
);
@@ -205,7 +205,6 @@ describe('WorkspaceEntityManager', () => {
IS_AI_ENABLED: false,
IS_APPLICATION_ENABLED: false,
IS_IMAP_SMTP_CALDAV_ENABLED: false,
IS_MORPH_RELATION_ENABLED: false,
IS_PAGE_LAYOUT_ENABLED: false,
IS_RECORD_PAGE_LAYOUT_ENABLED: false,
IS_PUBLIC_DOMAIN_ENABLED: false,
@@ -215,6 +214,7 @@ describe('WorkspaceEntityManager', () => {
IS_TIMELINE_ACTIVITY_MIGRATED: false,
IS_GLOBAL_WORKSPACE_DATASOURCE_ENABLED: false,
IS_READ_ON_REPLICA_ENABLED: false,
IS_WORKSPACE_CREATION_V2_ENABLED: false,
},
eventEmitterService: {
emitMutationEvent: jest.fn(),
@@ -23,6 +23,7 @@ import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { TwentyStandardApplicationService } from 'src/engine/workspace-manager/twenty-standard-application/services/twenty-standard-application.service';
import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service';
import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service';
@@ -130,6 +131,12 @@ describe('WorkspaceManagerService', () => {
deleteWorkspaceAllObjectMetadata: jest.fn(),
},
},
{
provide: TwentyStandardApplicationService,
useValue: {
synchronizeTwentyStandardApplicationOrThrow: jest.fn(),
},
},
{
provide: AgentService,
useValue: {
@@ -20,6 +20,7 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works
import { DevSeederModule } from 'src/engine/workspace-manager/dev-seeder/dev-seeder.module';
import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module';
import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module';
import { TwentyStandardApplicationModule } from 'src/engine/workspace-manager/twenty-standard-application/twenty-standard-application.module';
import { WorkspaceManagerService } from './workspace-manager.service';
@@ -35,6 +36,7 @@ import { WorkspaceManagerService } from './workspace-manager.service';
FeatureFlagModule,
PermissionsModule,
AiAgentModule,
TwentyStandardApplicationModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
TypeOrmModule.forFeature([UserWorkspaceEntity, WorkspaceEntity]),
RoleModule,
@@ -5,6 +5,8 @@ import { DataSource, Repository } from 'typeorm';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { fromApplicationEntityToFlatApplication } from 'src/engine/core-modules/application/utils/from-application-entity-to-flat-application.util';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
@@ -21,6 +23,7 @@ import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspac
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { prefillCoreViews } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-core-views';
import { standardObjectsPrefillData } from 'src/engine/workspace-manager/standard-objects-prefill-data/standard-objects-prefill-data';
import { TwentyStandardApplicationService } from 'src/engine/workspace-manager/twenty-standard-application/services/twenty-standard-application.service';
import { ADMIN_ROLE } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/admin-role';
import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service';
@@ -35,12 +38,11 @@ export class WorkspaceManagerService {
private readonly workspaceMigrationService: WorkspaceMigrationService,
private readonly objectMetadataService: ObjectMetadataService,
private readonly dataSourceService: DataSourceService,
private readonly workspaceSyncMetadataService: WorkspaceSyncMetadataService,
@InjectRepository(UserWorkspaceEntity)
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
private readonly roleService: RoleService,
private readonly userRoleService: UserRoleService,
private readonly featureFlagService: FeatureFlagService,
private readonly twentyStandardApplicationService: TwentyStandardApplicationService,
@InjectRepository(WorkspaceEntity)
private readonly workspaceRepository: Repository<WorkspaceEntity>,
@InjectRepository(RoleEntity)
@@ -50,6 +52,8 @@ export class WorkspaceManagerService {
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly applicationService: ApplicationService,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly featureFlagService: FeatureFlagService,
private readonly workspaceSyncMetadataService: WorkspaceSyncMetadataService,
) {}
public async init({
@@ -87,12 +91,39 @@ export class WorkspaceManagerService {
workspaceId,
});
// TODO later replace by twenty-standard installation aka workspaceMigration run
await this.workspaceSyncMetadataService.synchronize({
workspaceId,
dataSourceId: dataSourceMetadata.id,
featureFlags,
});
const isV2SyncEnabled =
featureFlags[FeatureFlagKey.IS_WORKSPACE_CREATION_V2_ENABLED];
if (isV2SyncEnabled) {
await this.twentyStandardApplicationService.synchronizeTwentyStandardApplicationOrThrow(
{
workspaceId,
},
);
} else {
await this.workspaceSyncMetadataService.synchronize({
workspaceId,
dataSourceId: dataSourceMetadata.id,
featureFlags,
});
const prefillStandardObjectsStart = performance.now();
await this.prefillWorkspaceWithStandardObjectsRecords({
dataSourceMetadata,
workspaceId,
featureFlags,
twentyStandardFlatApplication: fromApplicationEntityToFlatApplication(
twentyStandardApplication,
),
});
const prefillStandardObjectsEnd = performance.now();
this.logger.log(
`Prefill standard objects took ${prefillStandardObjectsEnd - prefillStandardObjectsStart}ms`,
);
}
const dataSourceMetadataCreationEnd = performance.now();
@@ -112,29 +143,19 @@ export class WorkspaceManagerService {
userId,
workspaceCustomFlatApplication,
});
const prefillStandardObjectsStart = performance.now();
await this.prefillWorkspaceWithStandardObjectsRecords(
dataSourceMetadata,
workspaceId,
featureFlags,
twentyStandardApplication,
);
const prefillStandardObjectsEnd = performance.now();
this.logger.log(
`Prefill standard objects took ${prefillStandardObjectsEnd - prefillStandardObjectsStart}ms`,
);
}
private async prefillWorkspaceWithStandardObjectsRecords(
dataSourceMetadata: DataSourceEntity,
workspaceId: string,
featureFlags: Record<string, boolean>,
twentyStandardFlatApplication: FlatApplication,
) {
private async prefillWorkspaceWithStandardObjectsRecords({
dataSourceMetadata,
workspaceId,
featureFlags,
twentyStandardFlatApplication,
}: {
dataSourceMetadata: DataSourceEntity;
workspaceId: string;
featureFlags: Record<string, boolean>;
twentyStandardFlatApplication: FlatApplication;
}) {
const { flatObjectMetadataMaps, flatFieldMetadataMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{