From 81fc9607125a6ade1a4e1f82b917913f4b9fddf4 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Sat, 28 Mar 2026 11:29:19 +0100 Subject: [PATCH] Deprecate dataSource table with dual-write to workspace.databaseSchema (#19059) ## Summary - Starts deprecation of the `core.dataSource` table by introducing a dual-write system: `DataSourceService.createDataSourceMetadata` now writes to both `core.dataSource` and `core.workspace.databaseSchema` - Migrates read sites (`WorkspaceDataSourceService.checkSchemaExists`, `WorkspaceSchemaFactory`, `MiddlewareService`, `WorkspacesMigrationCommandRunner`) to read from `workspace.databaseSchema` instead of querying the `dataSource` table - Removes the unused `databaseUrl` field from `WorkspaceEntity` and drops the column via migration - Adds a 1.20 upgrade command to backfill `workspace.databaseSchema` from `dataSource.schema` for existing workspaces --- .../src/metadata/generated/schema.graphql | 4 +- .../src/metadata/generated/schema.ts | 9 ++--- .../src/metadata/generated/types.ts | 3 -- .../src/generated-metadata/graphql.ts | 4 +- .../src/testing/mock-data/users.ts | 3 +- .../workspaces-migration.command-runner.ts | 2 +- ...3000-drop-workspace-database-url-column.ts | 39 +++++++++++++++++++ .../api/graphql/workspace-schema.factory.ts | 24 +++++++++--- .../workspace/workspace.entity.ts | 10 ++--- .../data-source/data-source.entity.ts | 3 ++ .../data-source/data-source.module.ts | 4 +- .../data-source/data-source.service.ts | 26 ++++++++++++- .../engine/middlewares/middleware.module.ts | 2 + .../engine/middlewares/middleware.service.ts | 27 ++++++++++--- .../workspace-entity-manager.spec.ts | 1 + .../workspace-datasource.module.ts | 10 ++++- .../workspace-datasource.service.ts | 31 ++++++++++++--- .../twenty-shared/src/types/FeatureFlagKey.ts | 1 + 18 files changed, 161 insertions(+), 42 deletions(-) create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/common/1774688563000-drop-workspace-database-url-column.ts diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql index 2e80259f5f..06fd2e50aa 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql @@ -780,8 +780,7 @@ type Workspace { viewGroups: [ViewGroup!] viewSorts: [ViewSort!] metadataVersion: Float! - databaseUrl: String! - databaseSchema: String! + databaseSchema: String subdomain: String! customDomain: String isGoogleAuthEnabled: Boolean! @@ -1749,6 +1748,7 @@ enum FeatureFlagKey { IS_CONNECTED_ACCOUNT_MIGRATED IS_GRAPHQL_QUERY_TIMING_ENABLED IS_RECORD_TABLE_WIDGET_ENABLED + IS_DATASOURCE_MIGRATED } type ClientConfig { diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts index 9a0450012f..4ccb220e3d 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts @@ -565,8 +565,7 @@ export interface Workspace { viewGroups?: ViewGroup[] viewSorts?: ViewSort[] metadataVersion: Scalars['Float'] - databaseUrl: Scalars['String'] - databaseSchema: Scalars['String'] + databaseSchema?: Scalars['String'] subdomain: Scalars['String'] customDomain?: Scalars['String'] isGoogleAuthEnabled: Scalars['Boolean'] @@ -1428,7 +1427,7 @@ export interface PublicFeatureFlag { __typename: 'PublicFeatureFlag' } -export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_AI_ENABLED' | 'IS_APPLICATION_ENABLED' | 'IS_MARKETPLACE_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAILING_DOMAIN_ENABLED' | 'IS_DASHBOARD_V2_ENABLED' | 'IS_ATTACHMENT_MIGRATED' | 'IS_NOTE_TARGET_MIGRATED' | 'IS_TASK_TARGET_MIGRATED' | 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_COMMAND_MENU_ITEM_ENABLED' | 'IS_NAVIGATION_MENU_ITEM_ENABLED' | 'IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED' | 'IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED' | 'IS_DRAFT_EMAIL_ENABLED' | 'IS_USAGE_ANALYTICS_ENABLED' | 'IS_RICH_TEXT_V1_MIGRATED' | 'IS_DIRECT_GRAPHQL_EXECUTION_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' | 'IS_CONNECTED_ACCOUNT_MIGRATED' | 'IS_GRAPHQL_QUERY_TIMING_ENABLED' | 'IS_RECORD_TABLE_WIDGET_ENABLED' +export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_AI_ENABLED' | 'IS_APPLICATION_ENABLED' | 'IS_MARKETPLACE_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAILING_DOMAIN_ENABLED' | 'IS_DASHBOARD_V2_ENABLED' | 'IS_ATTACHMENT_MIGRATED' | 'IS_NOTE_TARGET_MIGRATED' | 'IS_TASK_TARGET_MIGRATED' | 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_COMMAND_MENU_ITEM_ENABLED' | 'IS_NAVIGATION_MENU_ITEM_ENABLED' | 'IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED' | 'IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED' | 'IS_DRAFT_EMAIL_ENABLED' | 'IS_USAGE_ANALYTICS_ENABLED' | 'IS_RICH_TEXT_V1_MIGRATED' | 'IS_DIRECT_GRAPHQL_EXECUTION_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' | 'IS_CONNECTED_ACCOUNT_MIGRATED' | 'IS_GRAPHQL_QUERY_TIMING_ENABLED' | 'IS_RECORD_TABLE_WIDGET_ENABLED' | 'IS_DATASOURCE_MIGRATED' export interface ClientConfig { appVersion?: Scalars['String'] @@ -3656,7 +3655,6 @@ export interface WorkspaceGenqlSelection{ viewGroups?: ViewGroupGenqlSelection viewSorts?: ViewSortGenqlSelection metadataVersion?: boolean | number - databaseUrl?: boolean | number databaseSchema?: boolean | number subdomain?: boolean | number customDomain?: boolean | number @@ -9159,7 +9157,8 @@ export const enumFeatureFlagKey = { IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED: 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' as const, IS_CONNECTED_ACCOUNT_MIGRATED: 'IS_CONNECTED_ACCOUNT_MIGRATED' as const, IS_GRAPHQL_QUERY_TIMING_ENABLED: 'IS_GRAPHQL_QUERY_TIMING_ENABLED' as const, - IS_RECORD_TABLE_WIDGET_ENABLED: 'IS_RECORD_TABLE_WIDGET_ENABLED' as const + IS_RECORD_TABLE_WIDGET_ENABLED: 'IS_RECORD_TABLE_WIDGET_ENABLED' as const, + IS_DATASOURCE_MIGRATED: 'IS_DATASOURCE_MIGRATED' as const } export const enumConfigSource = { diff --git a/packages/twenty-client-sdk/src/metadata/generated/types.ts b/packages/twenty-client-sdk/src/metadata/generated/types.ts index e628df7f04..9547787a6b 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/types.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/types.ts @@ -1637,9 +1637,6 @@ export default { "metadataVersion": [ 11 ], - "databaseUrl": [ - 1 - ], "databaseSchema": [ 1 ], diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 875e5968df..f802e41ec1 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -1707,6 +1707,7 @@ export enum FeatureFlagKey { IS_COMMAND_MENU_ITEM_ENABLED = 'IS_COMMAND_MENU_ITEM_ENABLED', IS_CONNECTED_ACCOUNT_MIGRATED = 'IS_CONNECTED_ACCOUNT_MIGRATED', IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED', + IS_DATASOURCE_MIGRATED = 'IS_DATASOURCE_MIGRATED', IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED = 'IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED', IS_DIRECT_GRAPHQL_EXECUTION_ENABLED = 'IS_DIRECT_GRAPHQL_EXECUTION_ENABLED', IS_DRAFT_EMAIL_ENABLED = 'IS_DRAFT_EMAIL_ENABLED', @@ -6051,8 +6052,7 @@ export type Workspace = { createdAt: Scalars['DateTime']; currentBillingSubscription?: Maybe; customDomain?: Maybe; - databaseSchema: Scalars['String']; - databaseUrl: Scalars['String']; + databaseSchema?: Maybe; defaultRole?: Maybe; deletedAt?: Maybe; displayName?: Maybe; diff --git a/packages/twenty-front/src/testing/mock-data/users.ts b/packages/twenty-front/src/testing/mock-data/users.ts index 2bc2f042a5..31392d7f86 100644 --- a/packages/twenty-front/src/testing/mock-data/users.ts +++ b/packages/twenty-front/src/testing/mock-data/users.ts @@ -158,8 +158,7 @@ export const mockCurrentWorkspace = { }, ], workspaceMembersCount: 1, - databaseSchema: '', - databaseUrl: '', + databaseSchema: null, isTwoFactorAuthenticationEnforced: false, eventLogRetentionDays: 90, __typename: 'Workspace', diff --git a/packages/twenty-server/src/database/commands/command-runners/workspaces-migration.command-runner.ts b/packages/twenty-server/src/database/commands/command-runners/workspaces-migration.command-runner.ts index 8aa911a7a6..ea3b68f002 100644 --- a/packages/twenty-server/src/database/commands/command-runners/workspaces-migration.command-runner.ts +++ b/packages/twenty-server/src/database/commands/command-runners/workspaces-migration.command-runner.ts @@ -6,7 +6,7 @@ import { In, MoreThanOrEqual, type Repository } from 'typeorm'; import { MigrationCommandRunner } from 'src/database/commands/command-runners/migration.command-runner'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; -import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { type DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { GlobalWorkspaceDataSource } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource'; import { type 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'; diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1774688563000-drop-workspace-database-url-column.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1774688563000-drop-workspace-database-url-column.ts new file mode 100644 index 0000000000..eeac1eec29 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1774688563000-drop-workspace-database-url-column.ts @@ -0,0 +1,39 @@ +import { type MigrationInterface, type QueryRunner } from 'typeorm'; + +export class DropWorkspaceDatabaseUrlColumn1774688563000 + implements MigrationInterface +{ + name = 'DropWorkspaceDatabaseUrlColumn1774688563000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."workspace" DROP COLUMN IF EXISTS "databaseUrl"`, + ); + + await queryRunner.query( + `UPDATE "core"."workspace" SET "databaseSchema" = NULL WHERE "databaseSchema" = ''`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspace" ALTER COLUMN "databaseSchema" DROP NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspace" ALTER COLUMN "databaseSchema" DROP DEFAULT`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `UPDATE "core"."workspace" SET "databaseSchema" = '' WHERE "databaseSchema" IS NULL`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspace" ALTER COLUMN "databaseSchema" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspace" ALTER COLUMN "databaseSchema" SET DEFAULT ''`, + ); + + await queryRunner.query( + `ALTER TABLE "core"."workspace" ADD "databaseUrl" character varying NOT NULL DEFAULT ''`, + ); + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts index 6f8c8258ac..6cfcd2f2bd 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts @@ -1,14 +1,17 @@ import { Injectable } from '@nestjs/common'; import { makeExecutableSchema } from '@graphql-tools/schema'; +import { isNonEmptyString } from '@sniptt/guards'; import { GraphQLSchema, printSchema } from 'graphql'; import { gql } from 'graphql-tag'; +import { FeatureFlagKey } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { ScalarsExplorerService } from 'src/engine/api/graphql/services/scalars-explorer.service'; import { workspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/factories/factories'; import { WorkspaceResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory'; import { WorkspaceGraphQLSchemaGenerator } from 'src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { @@ -28,24 +31,33 @@ import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty @Injectable() export class WorkspaceSchemaFactory { constructor( - private readonly dataSourceService: DataSourceService, private readonly scalarsExplorerService: ScalarsExplorerService, private readonly workspaceGraphQLSchemaGenerator: WorkspaceGraphQLSchemaGenerator, private readonly workspaceResolverFactory: WorkspaceResolverFactory, private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, + private readonly featureFlagService: FeatureFlagService, + private readonly dataSourceService: DataSourceService, ) {} async createGraphQLSchema( workspace: WorkspaceEntity, applicationId?: string, ): Promise { - const dataSourcesMetadata = - await this.dataSourceService.getDataSourcesMetadataFromWorkspaceId( - workspace.id, - ); + const isDataSourceMigrated = await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IS_DATASOURCE_MIGRATED, + workspace.id, + ); - if (!dataSourcesMetadata || dataSourcesMetadata.length === 0) { + const hasSchema = isDataSourceMigrated + ? isNonEmptyString(workspace.databaseSchema) + : ( + await this.dataSourceService.getDataSourcesMetadataFromWorkspaceId( + workspace.id, + ) + ).length > 0; + + if (!hasSchema) { return new GraphQLSchema({}); } diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts index c940095da4..06a28f13d9 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts @@ -234,13 +234,9 @@ export class WorkspaceEntity { @Column({ default: 1 }) metadataVersion: number; - @Field() - @Column({ default: '' }) - databaseUrl: string; - - @Field() - @Column({ default: '' }) - databaseSchema: string; + @Field(() => String, { nullable: true }) + @Column({ type: 'varchar', nullable: true, default: null }) + databaseSchema: string | null; @Field() @Column({ unique: true }) diff --git a/packages/twenty-server/src/engine/metadata-modules/data-source/data-source.entity.ts b/packages/twenty-server/src/engine/metadata-modules/data-source/data-source.entity.ts index d7a87b57da..7ed45103a8 100644 --- a/packages/twenty-server/src/engine/metadata-modules/data-source/data-source.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/data-source/data-source.entity.ts @@ -14,6 +14,9 @@ import { WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/works export type DataSourceType = DataSourceOptions['type']; +// @deprecated - This entity is being deprecated in favor of storing +// databaseSchema directly on WorkspaceEntity. +// During the transition, writes go to both tables (dual-write). @Entity('dataSource') @Index('IDX_DATA_SOURCE_WORKSPACE_ID_CREATED_AT', ['workspaceId', 'createdAt']) export class DataSourceEntity extends WorkspaceRelatedEntity { diff --git a/packages/twenty-server/src/engine/metadata-modules/data-source/data-source.module.ts b/packages/twenty-server/src/engine/metadata-modules/data-source/data-source.module.ts index 09085746bb..196f829fa9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/data-source/data-source.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/data-source/data-source.module.ts @@ -1,11 +1,13 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; + import { DataSourceEntity } from './data-source.entity'; import { DataSourceService } from './data-source.service'; @Module({ - imports: [TypeOrmModule.forFeature([DataSourceEntity])], + imports: [TypeOrmModule.forFeature([DataSourceEntity, WorkspaceEntity])], providers: [DataSourceService], exports: [DataSourceService], }) diff --git a/packages/twenty-server/src/engine/metadata-modules/data-source/data-source.service.ts b/packages/twenty-server/src/engine/metadata-modules/data-source/data-source.service.ts index 55672ceee6..28ca348083 100644 --- a/packages/twenty-server/src/engine/metadata-modules/data-source/data-source.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/data-source/data-source.service.ts @@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { type FindManyOptions, Repository } from 'typeorm'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceException, DataSourceExceptionCode, @@ -10,22 +11,32 @@ import { import { DataSourceEntity } from './data-source.entity'; +// @deprecated - This service is being deprecated. During the transition, +// writes go to both the dataSource table and workspace table (dual-write). +// Reads should progressively migrate to use workspace.databaseSchema +// or the deterministic getWorkspaceSchemaName(workspaceId) utility. @Injectable() export class DataSourceService { constructor( @InjectRepository(DataSourceEntity) private readonly dataSourceMetadataRepository: Repository, + @InjectRepository(WorkspaceEntity) + private readonly workspaceRepository: Repository, ) {} async createDataSourceMetadata( workspaceId: string, workspaceSchema: string, ): Promise { - // TODO: Double check if this is the correct way to do this const dataSource = await this.dataSourceMetadataRepository.findOne({ where: { workspaceId }, }); + // Dual-write: always keep workspace.databaseSchema in sync + await this.workspaceRepository.update(workspaceId, { + databaseSchema: workspaceSchema, + }); + if (dataSource) { return dataSource; } @@ -36,12 +47,16 @@ export class DataSourceService { }); } + // @deprecated - Use workspace.activationStatus or workspace.databaseSchema + // to check if a workspace has been initialized instead. async getManyDataSourceMetadata( options: FindManyOptions = {}, ): Promise { return this.dataSourceMetadataRepository.find(options); } + // @deprecated - Use workspace.databaseSchema or + // getWorkspaceSchemaName(workspaceId) instead. async getDataSourcesMetadataFromWorkspaceId( workspaceId: string, ): Promise { @@ -51,6 +66,8 @@ export class DataSourceService { }); } + // @deprecated - Use workspace.databaseSchema or + // getWorkspaceSchemaName(workspaceId) instead. async getLastDataSourceMetadataFromWorkspaceId( workspaceId: string, ): Promise { @@ -60,6 +77,8 @@ export class DataSourceService { }); } + // @deprecated - Use workspace.databaseSchema or + // getWorkspaceSchemaName(workspaceId) instead. async getLastDataSourceMetadataFromWorkspaceIdOrFail( workspaceId: string, ): Promise { @@ -78,5 +97,10 @@ export class DataSourceService { async delete(workspaceId: string): Promise { await this.dataSourceMetadataRepository.delete({ workspaceId }); + + // Dual-write: clear workspace.databaseSchema on delete + await this.workspaceRepository.update(workspaceId, { + databaseSchema: null, + }); } } diff --git a/packages/twenty-server/src/engine/middlewares/middleware.module.ts b/packages/twenty-server/src/engine/middlewares/middleware.module.ts index d5a22a96b7..ac6b3cd1f0 100644 --- a/packages/twenty-server/src/engine/middlewares/middleware.module.ts +++ b/packages/twenty-server/src/engine/middlewares/middleware.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { TokenModule } from 'src/engine/core-modules/auth/token/token.module'; +import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; @@ -10,6 +11,7 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/ @Module({ imports: [ DataSourceModule, + FeatureFlagModule, WorkspaceCacheStorageModule, WorkspaceManyOrAllFlatEntityMapsCacheModule, TokenModule, diff --git a/packages/twenty-server/src/engine/middlewares/middleware.service.ts b/packages/twenty-server/src/engine/middlewares/middleware.service.ts index 4b46b7b244..9cd710ecd4 100644 --- a/packages/twenty-server/src/engine/middlewares/middleware.service.ts +++ b/packages/twenty-server/src/engine/middlewares/middleware.service.ts @@ -1,6 +1,8 @@ import { Injectable } from '@nestjs/common'; +import { isNonEmptyString } from '@sniptt/guards'; import { type Request, type Response } from 'express'; +import { FeatureFlagKey } from 'twenty-shared/types'; import { type APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations'; import { isDefined } from 'twenty-shared/utils'; @@ -9,6 +11,7 @@ import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filt import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service'; import { getAuthExceptionRestStatus } from 'src/engine/core-modules/auth/utils/get-auth-exception-rest-status.util'; import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; @@ -29,6 +32,7 @@ export class MiddlewareService { private readonly workspaceStorageCacheService: WorkspaceCacheStorageService, private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, private readonly dataSourceService: DataSourceService, + private readonly featureFlagService: FeatureFlagService, private readonly exceptionHandlerService: ExceptionHandlerService, private readonly jwtWrapperService: JwtWrapperService, ) {} @@ -106,13 +110,24 @@ export class MiddlewareService { ) : undefined; - const dataSourcesMetadata = data.workspace - ? await this.dataSourceService.getDataSourcesMetadataFromWorkspaceId( - data.workspace.id, - ) - : undefined; + if (!data.workspace) { + throw new Error('No data sources found'); + } - if (!dataSourcesMetadata || dataSourcesMetadata.length === 0) { + const isDataSourceMigrated = await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IS_DATASOURCE_MIGRATED, + data.workspace.id, + ); + + const hasSchema = isDataSourceMigrated + ? isNonEmptyString(data.workspace.databaseSchema) + : ( + await this.dataSourceService.getDataSourcesMetadataFromWorkspaceId( + data.workspace.id, + ) + ).length > 0; + + if (!hasSchema) { throw new Error('No data sources found'); } diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts index c97c56f41c..14346db839 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts @@ -256,6 +256,7 @@ describe('WorkspaceEntityManager', () => { IS_CONNECTED_ACCOUNT_MIGRATED: false, IS_GRAPHQL_QUERY_TIMING_ENABLED: false, IS_RECORD_TABLE_WIDGET_ENABLED: false, + IS_DATASOURCE_MIGRATED: false, }, userWorkspaceRoleMap: {}, eventEmitterService: { diff --git a/packages/twenty-server/src/engine/workspace-datasource/workspace-datasource.module.ts b/packages/twenty-server/src/engine/workspace-datasource/workspace-datasource.module.ts index 17d0a5cf3d..e04fd085db 100644 --- a/packages/twenty-server/src/engine/workspace-datasource/workspace-datasource.module.ts +++ b/packages/twenty-server/src/engine/workspace-datasource/workspace-datasource.module.ts @@ -1,12 +1,20 @@ import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; +import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { WorkspaceDataSourceService } from './workspace-datasource.service'; @Module({ - imports: [DataSourceModule, TypeORMModule], + imports: [ + TypeOrmModule.forFeature([WorkspaceEntity]), + TypeORMModule, + FeatureFlagModule, + DataSourceModule, + ], exports: [WorkspaceDataSourceService], providers: [WorkspaceDataSourceService], }) diff --git a/packages/twenty-server/src/engine/workspace-datasource/workspace-datasource.service.ts b/packages/twenty-server/src/engine/workspace-datasource/workspace-datasource.service.ts index a25c15d355..8487b853d6 100644 --- a/packages/twenty-server/src/engine/workspace-datasource/workspace-datasource.service.ts +++ b/packages/twenty-server/src/engine/workspace-datasource/workspace-datasource.service.ts @@ -1,9 +1,13 @@ import { Injectable } from '@nestjs/common'; -import { InjectDataSource } from '@nestjs/typeorm'; +import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; import { msg } from '@lingui/core/macro'; -import { type DataSource, type EntityManager } from 'typeorm'; +import { isNonEmptyString } from '@sniptt/guards'; +import { FeatureFlagKey } from 'twenty-shared/types'; +import { type DataSource, type EntityManager, Repository } from 'typeorm'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { PermissionsException, @@ -14,18 +18,35 @@ import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/ge @Injectable() export class WorkspaceDataSourceService { constructor( - private readonly dataSourceService: DataSourceService, + @InjectRepository(WorkspaceEntity) + private readonly workspaceRepository: Repository, @InjectDataSource() private readonly coreDataSource: DataSource, + private readonly featureFlagService: FeatureFlagService, + private readonly dataSourceService: DataSourceService, ) {} public async checkSchemaExists(workspaceId: string) { - const dataSource = + const isDataSourceMigrated = await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IS_DATASOURCE_MIGRATED, + workspaceId, + ); + + if (isDataSourceMigrated) { + const workspace = await this.workspaceRepository.findOne({ + select: ['databaseSchema'], + where: { id: workspaceId }, + }); + + return isNonEmptyString(workspace?.databaseSchema); + } + + const dataSources = await this.dataSourceService.getDataSourcesMetadataFromWorkspaceId( workspaceId, ); - return dataSource.length > 0; + return dataSources.length > 0; } /** diff --git a/packages/twenty-shared/src/types/FeatureFlagKey.ts b/packages/twenty-shared/src/types/FeatureFlagKey.ts index 1cee8cffb8..d2af983392 100644 --- a/packages/twenty-shared/src/types/FeatureFlagKey.ts +++ b/packages/twenty-shared/src/types/FeatureFlagKey.ts @@ -25,4 +25,5 @@ export enum FeatureFlagKey { IS_CONNECTED_ACCOUNT_MIGRATED = 'IS_CONNECTED_ACCOUNT_MIGRATED', IS_GRAPHQL_QUERY_TIMING_ENABLED = 'IS_GRAPHQL_QUERY_TIMING_ENABLED', IS_RECORD_TABLE_WIDGET_ENABLED = 'IS_RECORD_TABLE_WIDGET_ENABLED', + IS_DATASOURCE_MIGRATED = 'IS_DATASOURCE_MIGRATED', }