Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 75bd475209 fix: add RICH_TEXT to FIELD_NOT_OVERWRITTEN_AT_DRAFT to prevent table cell crash
https://sonarly.com/issue/6010?type=bug

Pressing any key while focused on a RICH_TEXT table cell (e.g., Notes body field) throws an unhandled error because `computeDraftValueFromString` has no handler for the RICH_TEXT field type.

Fix: **Two changes, both in `twenty-front`:**

1. **`FieldsNotOverwrittenAtDraft.ts`** — Added `FieldMetadataType.RICH_TEXT` to the `FIELD_NOT_OVERWRITTEN_AT_DRAFT` array, alphabetically between RATING and SELECT.

   This is the primary fix. When a user presses a key while focused on a table cell, `useInitDraftValue` checks this array. If the field type is listed, it uses `computeDraftValueFromFieldValue` (which handles all types via the existing record value) instead of `computeDraftValueFromString` (which tries to convert a single keystroke into a type-specific draft). RICH_TEXT fields use BlockNote/TipTap editors and cannot create a meaningful draft from a single character — same reason ADDRESS, PHONES, LINKS, SELECT, etc. are already in this list.

2. **`computeDraftValueFromString.ts`** — Fixed a stray `}` in the error message template literal. The string was `` `...${fieldDefinition.type}}` `` (double closing brace producing "RICH_TEXT}") instead of `` `...${fieldDefinition.type}` ``.
2026-03-28 15:21:44 +00:00
Charles BochetandGitHub 81fc960712 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
2026-03-28 11:29:19 +01:00
20 changed files with 163 additions and 43 deletions
@@ -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 {
@@ -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 = {
@@ -1637,9 +1637,6 @@ export default {
"metadataVersion": [
11
],
"databaseUrl": [
1
],
"databaseSchema": [
1
],
@@ -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<BillingSubscription>;
customDomain?: Maybe<Scalars['String']>;
databaseSchema: Scalars['String'];
databaseUrl: Scalars['String'];
databaseSchema?: Maybe<Scalars['String']>;
defaultRole?: Maybe<Role>;
deletedAt?: Maybe<Scalars['DateTime']>;
displayName?: Maybe<Scalars['String']>;
@@ -6,6 +6,7 @@ export const FIELD_NOT_OVERWRITTEN_AT_DRAFT = [
FieldMetadataType.LINKS,
FieldMetadataType.MULTI_SELECT,
FieldMetadataType.RATING,
FieldMetadataType.RICH_TEXT,
FieldMetadataType.SELECT,
FieldMetadataType.FILES,
];
@@ -83,7 +83,7 @@ export const computeDraftValueFromString = <FieldValue>({
}
throw new CustomError(
`Record field type not supported : ${fieldDefinition.type}}`,
`Record field type not supported : ${fieldDefinition.type}`,
'RECORD_FIELD_TYPE_NOT_SUPPORTED',
);
};
@@ -158,8 +158,7 @@ export const mockCurrentWorkspace = {
},
],
workspaceMembersCount: 1,
databaseSchema: '',
databaseUrl: '',
databaseSchema: null,
isTwoFactorAuthenticationEnforced: false,
eventLogRetentionDays: 90,
__typename: 'Workspace',
@@ -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';
@@ -0,0 +1,39 @@
import { type MigrationInterface, type QueryRunner } from 'typeorm';
export class DropWorkspaceDatabaseUrlColumn1774688563000
implements MigrationInterface
{
name = 'DropWorkspaceDatabaseUrlColumn1774688563000';
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
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 ''`,
);
}
}
@@ -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<GraphQLSchema> {
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({});
}
@@ -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 })
@@ -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 {
@@ -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],
})
@@ -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<DataSourceEntity>,
@InjectRepository(WorkspaceEntity)
private readonly workspaceRepository: Repository<WorkspaceEntity>,
) {}
async createDataSourceMetadata(
workspaceId: string,
workspaceSchema: string,
): Promise<DataSourceEntity> {
// 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<DataSourceEntity> = {},
): Promise<DataSourceEntity[]> {
return this.dataSourceMetadataRepository.find(options);
}
// @deprecated - Use workspace.databaseSchema or
// getWorkspaceSchemaName(workspaceId) instead.
async getDataSourcesMetadataFromWorkspaceId(
workspaceId: string,
): Promise<DataSourceEntity[]> {
@@ -51,6 +66,8 @@ export class DataSourceService {
});
}
// @deprecated - Use workspace.databaseSchema or
// getWorkspaceSchemaName(workspaceId) instead.
async getLastDataSourceMetadataFromWorkspaceId(
workspaceId: string,
): Promise<DataSourceEntity | null> {
@@ -60,6 +77,8 @@ export class DataSourceService {
});
}
// @deprecated - Use workspace.databaseSchema or
// getWorkspaceSchemaName(workspaceId) instead.
async getLastDataSourceMetadataFromWorkspaceIdOrFail(
workspaceId: string,
): Promise<DataSourceEntity> {
@@ -78,5 +97,10 @@ export class DataSourceService {
async delete(workspaceId: string): Promise<void> {
await this.dataSourceMetadataRepository.delete({ workspaceId });
// Dual-write: clear workspace.databaseSchema on delete
await this.workspaceRepository.update(workspaceId, {
databaseSchema: null,
});
}
}
@@ -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,
@@ -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');
}
@@ -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: {
@@ -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],
})
@@ -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<WorkspaceEntity>,
@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;
}
/**
@@ -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',
}