Compare commits

..
Author SHA1 Message Date
Sonarly Claude Code ee2f8e0226 fix: handle missing protocol in origin for getSubdomainAndDomainFromUrl
https://sonarly.com/issue/4546?type=bug

The `GetPublicWorkspaceDataByDomain` query crashes with `TypeError: Invalid URL` when the `origin` variable is a bare hostname without a protocol scheme (e.g., `hslmx.twenty.com` instead of `https://hslmx.twenty.com`).

Fix: Added URL protocol normalization in `DomainServerConfigService.getSubdomainAndDomainFromUrl()`. When the input URL string lacks a protocol scheme (e.g., `hslmx.twenty.com` instead of `https://hslmx.twenty.com`), the method now prepends `https://` before passing it to `new URL()`. This prevents the `TypeError: Invalid URL` that occurs when API clients call `GetPublicWorkspaceDataByDomain` with a bare hostname.

The check `url.includes('://')` is used rather than a regex to stay consistent with the codebase's simple string operations. Full URLs (from browser `window.location.origin`) pass through unchanged.

Added 3 test cases to the existing test file:
1. Extracts subdomain from a full URL with protocol (existing behavior preserved)
2. Handles URL without protocol scheme (the bug fix)
3. Returns custom domain for non-front domain URLs
2026-03-27 20:21:58 +00:00
21 changed files with 117 additions and 207 deletions
@@ -780,7 +780,8 @@ type Workspace {
viewGroups: [ViewGroup!]
viewSorts: [ViewSort!]
metadataVersion: Float!
databaseSchema: String
databaseUrl: String!
databaseSchema: String!
subdomain: String!
customDomain: String
isGoogleAuthEnabled: Boolean!
@@ -1748,7 +1749,6 @@ enum FeatureFlagKey {
IS_CONNECTED_ACCOUNT_MIGRATED
IS_GRAPHQL_QUERY_TIMING_ENABLED
IS_RECORD_TABLE_WIDGET_ENABLED
IS_DATASOURCE_MIGRATED
}
type ClientConfig {
@@ -565,7 +565,8 @@ export interface Workspace {
viewGroups?: ViewGroup[]
viewSorts?: ViewSort[]
metadataVersion: Scalars['Float']
databaseSchema?: Scalars['String']
databaseUrl: Scalars['String']
databaseSchema: Scalars['String']
subdomain: Scalars['String']
customDomain?: Scalars['String']
isGoogleAuthEnabled: Scalars['Boolean']
@@ -1427,7 +1428,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' | 'IS_DATASOURCE_MIGRATED'
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 interface ClientConfig {
appVersion?: Scalars['String']
@@ -3655,6 +3656,7 @@ export interface WorkspaceGenqlSelection{
viewGroups?: ViewGroupGenqlSelection
viewSorts?: ViewSortGenqlSelection
metadataVersion?: boolean | number
databaseUrl?: boolean | number
databaseSchema?: boolean | number
subdomain?: boolean | number
customDomain?: boolean | number
@@ -9157,8 +9159,7 @@ 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_DATASOURCE_MIGRATED: 'IS_DATASOURCE_MIGRATED' as const
IS_RECORD_TABLE_WIDGET_ENABLED: 'IS_RECORD_TABLE_WIDGET_ENABLED' as const
}
export const enumConfigSource = {
@@ -1637,6 +1637,9 @@ export default {
"metadataVersion": [
11
],
"databaseUrl": [
1
],
"databaseSchema": [
1
],
@@ -1707,7 +1707,6 @@ 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',
@@ -6052,7 +6051,8 @@ export type Workspace = {
createdAt: Scalars['DateTime'];
currentBillingSubscription?: Maybe<BillingSubscription>;
customDomain?: Maybe<Scalars['String']>;
databaseSchema?: Maybe<Scalars['String']>;
databaseSchema: Scalars['String'];
databaseUrl: Scalars['String'];
defaultRole?: Maybe<Role>;
deletedAt?: Maybe<Scalars['DateTime']>;
displayName?: Maybe<Scalars['String']>;
@@ -158,7 +158,8 @@ export const mockCurrentWorkspace = {
},
],
workspaceMembersCount: 1,
databaseSchema: null,
databaseSchema: '',
databaseUrl: '',
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 { type DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { 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';
@@ -1,39 +0,0 @@
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,17 +1,14 @@
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 {
@@ -31,33 +28,24 @@ 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 isDataSourceMigrated = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_DATASOURCE_MIGRATED,
workspace.id,
);
const dataSourcesMetadata =
await this.dataSourceService.getDataSourcesMetadataFromWorkspaceId(
workspace.id,
);
const hasSchema = isDataSourceMigrated
? isNonEmptyString(workspace.databaseSchema)
: (
await this.dataSourceService.getDataSourcesMetadataFromWorkspaceId(
workspace.id,
)
).length > 0;
if (!hasSchema) {
if (!dataSourcesMetadata || dataSourcesMetadata.length === 0) {
return new GraphQLSchema({});
}
@@ -42,6 +42,74 @@ describe('SubdomainManagerService', () => {
twentyConfigService = module.get<TwentyConfigService>(TwentyConfigService);
});
describe('getSubdomainAndDomainFromUrl', () => {
it('should extract subdomain from a full URL with protocol', () => {
jest
.spyOn(twentyConfigService, 'get')
.mockImplementation((key: string) => {
const env = {
FRONTEND_URL: 'https://twenty.com',
DEFAULT_SUBDOMAIN: 'app',
};
// @ts-expect-error legacy noImplicitAny
return env[key];
});
const result =
domainServerConfigService.getSubdomainAndDomainFromUrl(
'https://myworkspace.twenty.com',
);
expect(result.subdomain).toBe('myworkspace');
expect(result.domain).toBeNull();
});
it('should handle URL without protocol scheme', () => {
jest
.spyOn(twentyConfigService, 'get')
.mockImplementation((key: string) => {
const env = {
FRONTEND_URL: 'https://twenty.com',
DEFAULT_SUBDOMAIN: 'app',
};
// @ts-expect-error legacy noImplicitAny
return env[key];
});
const result =
domainServerConfigService.getSubdomainAndDomainFromUrl(
'myworkspace.twenty.com',
);
expect(result.subdomain).toBe('myworkspace');
expect(result.domain).toBeNull();
});
it('should return custom domain for non-front domain URLs', () => {
jest
.spyOn(twentyConfigService, 'get')
.mockImplementation((key: string) => {
const env = {
FRONTEND_URL: 'https://twenty.com',
DEFAULT_SUBDOMAIN: 'app',
};
// @ts-expect-error legacy noImplicitAny
return env[key];
});
const result =
domainServerConfigService.getSubdomainAndDomainFromUrl(
'custom.example.com',
);
expect(result.subdomain).toBeUndefined();
expect(result.domain).toBe('custom.example.com');
});
});
describe('buildBaseUrl', () => {
it('should build the base URL from environment variables', () => {
jest
@@ -46,7 +46,9 @@ export class DomainServerConfigService {
}
getSubdomainAndDomainFromUrl = (url: string) => {
const { hostname: originHostname } = new URL(url);
const urlWithProtocol = url.includes('://') ? url : `https://${url}`;
const { hostname: originHostname } = new URL(urlWithProtocol);
const frontDomain = this.getFrontUrl().hostname;
@@ -16,9 +16,7 @@ import {
type ListLayerVersionsCommandInput,
LogType,
PublishLayerVersionCommand,
ResourceConflictException,
ResourceNotFoundException,
TooManyRequestsException,
waitUntilFunctionActiveV2,
} from '@aws-sdk/client-lambda';
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
@@ -394,13 +392,7 @@ export class LambdaDriver implements LogicFunctionDriver {
MemorySize: YARN_INSTALL_LAMBDA_MEMORY_MB,
};
try {
await lambdaClient.send(new CreateFunctionCommand(params));
} catch (error) {
if (!(error instanceof ResourceConflictException)) {
throw error;
}
}
await lambdaClient.send(new CreateFunctionCommand(params));
} finally {
await temporaryDirManager.clean();
}
@@ -489,13 +481,7 @@ export class LambdaDriver implements LogicFunctionDriver {
MemorySize: BUILDER_LAMBDA_MEMORY_MB,
};
try {
await lambdaClient.send(new CreateFunctionCommand(params));
} catch (error) {
if (!(error instanceof ResourceConflictException)) {
throw error;
}
}
await lambdaClient.send(new CreateFunctionCommand(params));
} finally {
await temporaryDirManager.clean();
}
@@ -813,27 +799,7 @@ export class LambdaDriver implements LogicFunctionDriver {
FunctionName: flatLogicFunction.id,
});
try {
await (await this.getLambdaClient()).send(deleteFunctionCommand);
} catch (error) {
if (
error instanceof ResourceNotFoundException ||
error instanceof ResourceConflictException
) {
// Already deleted by another concurrent worker, or function
// is in a pending state — safe to proceed.
return;
}
if (error instanceof TooManyRequestsException) {
// Rate limited — the function may or may not still exist.
// The subsequent CreateFunctionCommand will handle both cases
// (create succeeds, or ResourceConflictException if it exists).
return;
}
throw error;
}
await (await this.getLambdaClient()).send(deleteFunctionCommand);
}
}
@@ -972,14 +938,7 @@ export class LambdaDriver implements LogicFunctionDriver {
const command = new CreateFunctionCommand(params);
try {
await (await this.getLambdaClient()).send(command);
} catch (error) {
if (!(error instanceof ResourceConflictException)) {
throw error;
}
// Function was concurrently created by another worker — safe to proceed.
}
await (await this.getLambdaClient()).send(command);
} finally {
await temporaryDirManager.clean();
}
@@ -234,9 +234,13 @@ export class WorkspaceEntity {
@Column({ default: 1 })
metadataVersion: number;
@Field(() => String, { nullable: true })
@Column({ type: 'varchar', nullable: true, default: null })
databaseSchema: string | null;
@Field()
@Column({ default: '' })
databaseUrl: string;
@Field()
@Column({ default: '' })
databaseSchema: string;
@Field()
@Column({ unique: true })
@@ -14,9 +14,6 @@ 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,13 +1,11 @@
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, WorkspaceEntity])],
imports: [TypeOrmModule.forFeature([DataSourceEntity])],
providers: [DataSourceService],
exports: [DataSourceService],
})
@@ -3,7 +3,6 @@ import { InjectRepository } from '@nestjs/typeorm';
import { type FindManyOptions, Repository } from 'typeorm';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import {
DataSourceException,
DataSourceExceptionCode,
@@ -11,32 +10,22 @@ 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;
}
@@ -47,16 +36,12 @@ 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[]> {
@@ -66,8 +51,6 @@ export class DataSourceService {
});
}
// @deprecated - Use workspace.databaseSchema or
// getWorkspaceSchemaName(workspaceId) instead.
async getLastDataSourceMetadataFromWorkspaceId(
workspaceId: string,
): Promise<DataSourceEntity | null> {
@@ -77,8 +60,6 @@ export class DataSourceService {
});
}
// @deprecated - Use workspace.databaseSchema or
// getWorkspaceSchemaName(workspaceId) instead.
async getLastDataSourceMetadataFromWorkspaceIdOrFail(
workspaceId: string,
): Promise<DataSourceEntity> {
@@ -97,10 +78,5 @@ 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,7 +1,6 @@
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';
@@ -11,7 +10,6 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
@Module({
imports: [
DataSourceModule,
FeatureFlagModule,
WorkspaceCacheStorageModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
TokenModule,
@@ -1,8 +1,6 @@
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';
@@ -11,7 +9,6 @@ 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';
@@ -32,7 +29,6 @@ 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,
) {}
@@ -110,24 +106,13 @@ export class MiddlewareService {
)
: undefined;
if (!data.workspace) {
throw new Error('No data sources found');
}
const dataSourcesMetadata = data.workspace
? await this.dataSourceService.getDataSourcesMetadataFromWorkspaceId(
data.workspace.id,
)
: undefined;
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) {
if (!dataSourcesMetadata || dataSourcesMetadata.length === 0) {
throw new Error('No data sources found');
}
@@ -256,7 +256,6 @@ 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,20 +1,12 @@
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: [
TypeOrmModule.forFeature([WorkspaceEntity]),
TypeORMModule,
FeatureFlagModule,
DataSourceModule,
],
imports: [DataSourceModule, TypeORMModule],
exports: [WorkspaceDataSourceService],
providers: [WorkspaceDataSourceService],
})
@@ -1,13 +1,9 @@
import { Injectable } from '@nestjs/common';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { InjectDataSource } from '@nestjs/typeorm';
import { msg } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { FeatureFlagKey } from 'twenty-shared/types';
import { type DataSource, type EntityManager, Repository } from 'typeorm';
import { type DataSource, type EntityManager } 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,
@@ -18,35 +14,18 @@ import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/ge
@Injectable()
export class WorkspaceDataSourceService {
constructor(
@InjectRepository(WorkspaceEntity)
private readonly workspaceRepository: Repository<WorkspaceEntity>,
private readonly dataSourceService: DataSourceService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
private readonly featureFlagService: FeatureFlagService,
private readonly dataSourceService: DataSourceService,
) {}
public async checkSchemaExists(workspaceId: string) {
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 =
const dataSource =
await this.dataSourceService.getDataSourcesMetadataFromWorkspaceId(
workspaceId,
);
return dataSources.length > 0;
return dataSource.length > 0;
}
/**
@@ -25,5 +25,4 @@ 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',
}