From 463dee3fe694f63d1975d7585587b7e2616a770a Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Wed, 7 May 2025 10:42:51 +0200 Subject: [PATCH] Remove usages of connectToDataSource and use workspaceDataSource (#11873) In this PR we are 1. cleaning typeORM service by removing connectToDataSource method 2. using workspaceDataSource instead of mainDataSource when possible, and replacing raw SQL with workspaceRepository methods to use --- ...ded-workspaces-migration.command-runner.ts | 8 +- .../data-seed-demo-workspace.service.ts | 2 +- .../data-seed-dev-workspace.command.ts | 34 +++--- .../commands/database-command.module.ts | 2 + ...migrate-rich-text-content-patch.command.ts | 7 +- ...date-workflow-trigger-type-enum.command.ts | 10 +- .../src/database/typeorm-seeds/core/index.ts | 18 ++- .../database/typeorm-seeds/core/workspaces.ts | 10 +- .../src/database/typeorm/typeorm.service.ts | 77 ------------ .../interfaces/base-resolver-service.ts | 15 +-- .../api/rest/core/rest-api-core-v2.service.ts | 5 +- .../auth/services/google-apis.service.ts | 4 +- .../auth/services/microsoft-apis.service.ts | 4 +- .../auth/strategies/jwt.auth.strategy.spec.ts | 57 ++++----- .../auth/strategies/jwt.auth.strategy.ts | 32 +++-- .../user-workspace.service.spec.ts | 51 ++++---- .../user-workspace/user-workspace.service.ts | 46 +++---- .../user/services/user.service.ts | 23 ++-- .../field-metadata/field-metadata.service.ts | 112 +++++++++--------- .../distant-table/distant-table.service.ts | 18 ++- .../foreign-table/foreign-table.service.ts | 8 +- .../remote-table/remote-table.service.ts | 8 +- .../src/engine/seeder/seeder.service.ts | 9 +- .../datasource/workspace.datasource.ts | 10 +- .../workspace-entity-manager.ts | 47 +++++--- .../twenty-orm/twenty-orm-global.manager.ts | 9 +- .../workspace-cache-storage.service.ts | 10 +- .../workspace-datasource.service.ts | 37 ++---- .../standard-objects-prefill-data.ts | 50 ++++---- .../workspace-health.service.ts | 5 - .../workspace-manager.service.ts | 24 ++-- .../workspace-migration-runner.service.ts | 12 +- ...ct-records-permissions.integration-spec.ts | 1 - 33 files changed, 324 insertions(+), 441 deletions(-) diff --git a/packages/twenty-server/src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner.ts b/packages/twenty-server/src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner.ts index 5fc64dff1..6e4a4a090 100644 --- a/packages/twenty-server/src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner.ts +++ b/packages/twenty-server/src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner.ts @@ -1,7 +1,7 @@ import chalk from 'chalk'; import { Option } from 'nest-commander'; -import { In, MoreThanOrEqual, Repository } from 'typeorm'; import { WorkspaceActivationStatus } from 'twenty-shared/workspace'; +import { In, MoreThanOrEqual, Repository } from 'typeorm'; import { MigrationCommandRunner } from 'src/database/commands/command-runners/migration.command-runner'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -138,10 +138,10 @@ export abstract class ActiveOrSuspendedWorkspacesMigrationCommandRunner< try { const dataSource = - await this.twentyORMGlobalManager.getDataSourceForWorkspace( + await this.twentyORMGlobalManager.getDataSourceForWorkspace({ workspaceId, - false, - ); + shouldFailIfMetadataNotFound: false, + }); await this.runOnWorkspace({ options, diff --git a/packages/twenty-server/src/database/commands/data-seed-demo-workspace/services/data-seed-demo-workspace.service.ts b/packages/twenty-server/src/database/commands/data-seed-demo-workspace/services/data-seed-demo-workspace.service.ts index 0ca70c13f..87b55f40b 100644 --- a/packages/twenty-server/src/database/commands/data-seed-demo-workspace/services/data-seed-demo-workspace.service.ts +++ b/packages/twenty-server/src/database/commands/data-seed-demo-workspace/services/data-seed-demo-workspace.service.ts @@ -46,7 +46,7 @@ export class DataSeedDemoWorkspaceService { const appVersion = this.twentyConfigService.get('APP_VERSION'); await seedCoreSchema({ - workspaceDataSource: rawDataSource, + dataSource: rawDataSource, workspaceId, appVersion, seedBilling: false, diff --git a/packages/twenty-server/src/database/commands/data-seed-dev-workspace.command.ts b/packages/twenty-server/src/database/commands/data-seed-dev-workspace.command.ts index 517710899..3ea8230a6 100644 --- a/packages/twenty-server/src/database/commands/data-seed-dev-workspace.command.ts +++ b/packages/twenty-server/src/database/commands/data-seed-dev-workspace.command.ts @@ -30,9 +30,6 @@ import { seedPeople } from 'src/database/typeorm-seeds/workspace/seedPeople'; import { seedWorkspaceMember } from 'src/database/typeorm-seeds/workspace/workspace-members'; import { rawDataSource } from 'src/database/typeorm/raw/raw.datasource'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; -import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator'; -import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; -import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; @@ -45,12 +42,12 @@ import { SURVEY_RESULTS_METADATA_SEEDS } from 'src/engine/seeder/metadata-seeds/ import { SeederService } from 'src/engine/seeder/seeder.service'; import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; import { shouldSeedWorkspaceFavorite } from 'src/engine/utils/should-seed-workspace-favorite'; +import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; import { createWorkspaceViews } from 'src/engine/workspace-manager/standard-objects-prefill-data/create-workspace-views'; import { seedViewWithDemoData } from 'src/engine/workspace-manager/standard-objects-prefill-data/seed-view-with-demo-data'; import { opportunitiesTableByStageView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/opportunity-table-by-stage.view'; import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service'; import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; - // TODO: implement dry-run @Command({ name: 'workspace:seed:dev', @@ -66,11 +63,10 @@ export class DataSeedWorkspaceCommand extends CommandRunner { private readonly typeORMService: TypeORMService, private readonly fieldMetadataService: FieldMetadataService, private readonly objectMetadataService: ObjectMetadataService, - @InjectCacheStorage(CacheStorageNamespace.EngineWorkspace) - private readonly workspaceSchemaCache: CacheStorageService, private readonly seederService: SeederService, private readonly workspaceManagerService: WorkspaceManagerService, private readonly twentyConfigService: TwentyConfigService, + private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, ) { super(); } @@ -92,7 +88,13 @@ export class DataSeedWorkspaceCommand extends CommandRunner { } async createWorkspaceSchema(workspaceId: string) { - await this.workspaceSchemaCache.flush(); + const workspaceCachedMetadataVersion = + await this.workspaceCacheStorageService.getMetadataVersion(workspaceId); + + await this.workspaceCacheStorageService.flush( + workspaceId, + workspaceCachedMetadataVersion, + ); await rawDataSource.initialize(); @@ -100,7 +102,7 @@ export class DataSeedWorkspaceCommand extends CommandRunner { const appVersion = this.twentyConfigService.get('APP_VERSION'); await seedCoreSchema({ - workspaceDataSource: rawDataSource, + dataSource: rawDataSource, workspaceId, seedBilling: isBillingEnabled, appVersion, @@ -117,10 +119,9 @@ export class DataSeedWorkspaceCommand extends CommandRunner { workspaceId, ); - const workspaceDataSource = - await this.typeORMService.connectToDataSource(dataSourceMetadata); + const mainDataSource = this.typeORMService.getMainDataSource(); - if (!workspaceDataSource) { + if (!mainDataSource) { throw new Error('Could not connect to workspace data source'); } @@ -140,10 +141,7 @@ export class DataSeedWorkspaceCommand extends CommandRunner { workspaceId, ); - await this.seedStandardObjectRecords( - workspaceDataSource, - dataSourceMetadata, - ); + await this.seedStandardObjectRecords(mainDataSource, dataSourceMetadata); await this.seederService.seedCustomObjects( dataSourceMetadata.id, @@ -161,15 +159,13 @@ export class DataSeedWorkspaceCommand extends CommandRunner { } catch (error) { this.logger.error(error); } - - await this.typeORMService.disconnectFromDataSource(dataSourceMetadata.id); } async seedStandardObjectRecords( - workspaceDataSource: DataSource, + mainDataSource: DataSource, dataSourceMetadata: DataSourceEntity, ) { - await workspaceDataSource.transaction( + await mainDataSource.transaction( async (entityManager: WorkspaceEntityManager) => { const { objectMetadataStandardIdToIdMap } = await this.objectMetadataService.getObjectMetadataStandardIdToIdMap( diff --git a/packages/twenty-server/src/database/commands/database-command.module.ts b/packages/twenty-server/src/database/commands/database-command.module.ts index 736ba0ba2..a25121ab8 100644 --- a/packages/twenty-server/src/database/commands/database-command.module.ts +++ b/packages/twenty-server/src/database/commands/database-command.module.ts @@ -8,6 +8,7 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module'; import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; import { SeederModule } from 'src/engine/seeder/seeder.module'; +import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module'; @Module({ @@ -21,6 +22,7 @@ import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-m SeederModule, WorkspaceManagerModule, DataSourceModule, + WorkspaceCacheStorageModule, ], providers: [DataSeedWorkspaceCommand, ConfirmationQuestion], }) diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/0-43/0-43-migrate-rich-text-content-patch.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/0-43/0-43-migrate-rich-text-content-patch.command.ts index 1bc5d1a14..62b413000 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/0-43/0-43-migrate-rich-text-content-patch.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/0-43/0-43-migrate-rich-text-content-patch.command.ts @@ -227,12 +227,11 @@ export class MigrateRichTextContentPatchCommand extends ActiveOrSuspendedWorkspa const schemaName = this.workspaceDataSourceService.getSchemaName(workspaceId); - const failOnMetadataCacheMiss = false; const workspaceDataSource = - await this.twentyORMGlobalManager.getDataSourceForWorkspace( + await this.twentyORMGlobalManager.getDataSourceForWorkspace({ workspaceId, - failOnMetadataCacheMiss, - ); + shouldFailIfMetadataNotFound: false, + }); const rows = await workspaceDataSource.query( `SELECT id, "${richTextField.name}" FROM "${schemaName}"."${computeTableName(objectMetadata.nameSingular, objectMetadata.isCustom)}" WHERE "${richTextField.name}" IS NOT NULL`, diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/0-51/0-51-update-workflow-trigger-type-enum.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/0-51/0-51-update-workflow-trigger-type-enum.command.ts index 436078a62..da7239398 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/0-51/0-51-update-workflow-trigger-type-enum.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/0-51/0-51-update-workflow-trigger-type-enum.command.ts @@ -1,18 +1,18 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Command } from 'nest-commander'; -import { Repository } from 'typeorm'; import { isDefined } from 'twenty-shared/utils'; +import { Repository } from 'typeorm'; import { ActiveOrSuspendedWorkspacesMigrationCommandRunner, RunOnWorkspaceArgs, } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; -import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { computeTableName } from 'src/engine/utils/compute-table-name.util'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; @Command({ name: 'upgrade:0-51:upgrade-created-by-enum', @@ -43,7 +43,9 @@ export class UpgradeCreatedByEnumCommand extends ActiveOrSuspendedWorkspacesMigr this.workspaceDataSourceService.getSchemaName(workspaceId); const workspaceDataSource = - await this.twentyORMGlobalManager.getDataSourceForWorkspace(workspaceId); + await this.twentyORMGlobalManager.getDataSourceForWorkspace({ + workspaceId, + }); const objectMetadatas = await this.objectMetadataRepository.find({ where: { diff --git a/packages/twenty-server/src/database/typeorm-seeds/core/index.ts b/packages/twenty-server/src/database/typeorm-seeds/core/index.ts index 4bdab4b81..bf816a789 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/core/index.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/core/index.ts @@ -7,7 +7,7 @@ import { seedUsers } from 'src/database/typeorm-seeds/core/users'; import { seedWorkspaces } from 'src/database/typeorm-seeds/core/workspaces'; type SeedCoreSchemaArgs = { - workspaceDataSource: DataSource; + dataSource: DataSource; workspaceId: string; appVersion: string | undefined; seedBilling?: boolean; @@ -16,7 +16,7 @@ type SeedCoreSchemaArgs = { export const seedCoreSchema = async ({ appVersion, - workspaceDataSource, + dataSource, workspaceId, seedBilling = true, seedFeatureFlags: shouldSeedFeatureFlags = true, @@ -24,23 +24,19 @@ export const seedCoreSchema = async ({ const schemaName = 'core'; await seedWorkspaces({ - workspaceDataSource, + dataSource, schemaName, workspaceId, appVersion, }); - await seedUsers(workspaceDataSource, schemaName); - await seedUserWorkspaces(workspaceDataSource, schemaName, workspaceId); + await seedUsers(dataSource, schemaName); + await seedUserWorkspaces(dataSource, schemaName, workspaceId); if (shouldSeedFeatureFlags) { - await seedFeatureFlags(workspaceDataSource, schemaName, workspaceId); + await seedFeatureFlags(dataSource, schemaName, workspaceId); } if (seedBilling) { - await seedBillingSubscriptions( - workspaceDataSource, - schemaName, - workspaceId, - ); + await seedBillingSubscriptions(dataSource, schemaName, workspaceId); } }; diff --git a/packages/twenty-server/src/database/typeorm-seeds/core/workspaces.ts b/packages/twenty-server/src/database/typeorm-seeds/core/workspaces.ts index 80b15ed59..d0744088c 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/core/workspaces.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/core/workspaces.ts @@ -10,7 +10,7 @@ export const SEED_APPLE_WORKSPACE_ID = '20202020-1c25-4d02-bf25-6aeccf7ea419'; export const SEED_ACME_WORKSPACE_ID = '3b8e6458-5fc1-4e63-8563-008ccddaa6db'; export type SeedWorkspaceArgs = { - workspaceDataSource: DataSource; + dataSource: DataSource; schemaName: string; workspaceId: string; appVersion: string | undefined; @@ -33,7 +33,7 @@ type WorkspaceSeederFields = Pick< export const seedWorkspaces = async ({ schemaName, - workspaceDataSource, + dataSource, workspaceId, appVersion, }: SeedWorkspaceArgs) => { @@ -60,7 +60,7 @@ export const seedWorkspaces = async ({ }, }; - await workspaceDataSource + await dataSource .createQueryBuilder() .insert() .into(`${schemaName}.${tableName}`, workspaceSeederFields) @@ -70,11 +70,11 @@ export const seedWorkspaces = async ({ }; export const deleteWorkspaces = async ( - workspaceDataSource: DataSource, + dataSource: DataSource, schemaName: string, workspaceId: string, ) => { - await workspaceDataSource + await dataSource .createQueryBuilder() .delete() .from(`${schemaName}.${tableName}`) diff --git a/packages/twenty-server/src/database/typeorm/typeorm.service.ts b/packages/twenty-server/src/database/typeorm/typeorm.service.ts index dcf12bc27..4ddf7c474 100644 --- a/packages/twenty-server/src/database/typeorm/typeorm.service.ts +++ b/packages/twenty-server/src/database/typeorm/typeorm.service.ts @@ -22,12 +22,9 @@ import { TwoFactorMethod } from 'src/engine/core-modules/two-factor-method/two-f import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { User } from 'src/engine/core-modules/user/user.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; @Injectable() export class TypeORMService implements OnModuleInit, OnModuleDestroy { private mainDataSource: DataSource; - private dataSources: Map = new Map(); - private isDatasourceInitializing: Map = new Map(); constructor(private readonly twentyConfigService: TwentyConfigService) { this.mainDataSource = new DataSource({ @@ -73,75 +70,6 @@ export class TypeORMService implements OnModuleInit, OnModuleDestroy { return this.mainDataSource; } - public async connectToDataSource( - dataSource: DataSourceEntity, - ): Promise { - const isMultiDatasourceEnabled = false; - - if (isMultiDatasourceEnabled) { - // Wait for a bit before trying again if another initialization is in progress - while (this.isDatasourceInitializing.get(dataSource.id)) { - await new Promise((resolve) => setTimeout(resolve, 10)); - } - - if (this.dataSources.has(dataSource.id)) { - return this.dataSources.get(dataSource.id); - } - - this.isDatasourceInitializing.set(dataSource.id, true); - - try { - const dataSourceInstance = - await this.createAndInitializeDataSource(dataSource); - - this.dataSources.set(dataSource.id, dataSourceInstance); - - return dataSourceInstance; - } finally { - this.isDatasourceInitializing.delete(dataSource.id); - } - } - - return this.mainDataSource; - } - - private async createAndInitializeDataSource( - dataSource: DataSourceEntity, - ): Promise { - const schema = dataSource.schema; - - const workspaceDataSource = new DataSource({ - url: dataSource.url ?? this.twentyConfigService.get('PG_DATABASE_URL'), - type: 'postgres', - logging: - this.twentyConfigService.get('NODE_ENV') === NodeEnvironment.development - ? ['query', 'error'] - : ['error'], - schema, - ssl: this.twentyConfigService.get('PG_SSL_ALLOW_SELF_SIGNED') - ? { - rejectUnauthorized: false, - } - : undefined, - }); - - await workspaceDataSource.initialize(); - - return workspaceDataSource; - } - - public async disconnectFromDataSource(dataSourceId: string) { - if (!this.dataSources.has(dataSourceId)) { - return; - } - - const dataSource = this.dataSources.get(dataSourceId); - - await dataSource?.destroy(); - - this.dataSources.delete(dataSourceId); - } - public async createSchema(schemaName: string): Promise { const queryRunner = this.mainDataSource.createQueryRunner(); @@ -168,10 +96,5 @@ export class TypeORMService implements OnModuleInit, OnModuleDestroy { async onModuleDestroy() { // Destroy main data source "default" schema await this.mainDataSource.destroy(); - - // Destroy all workspace data sources - for (const [, dataSource] of this.dataSources) { - await dataSource.destroy(); - } } } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts index 740558d7a..bd0925653 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts @@ -85,12 +85,13 @@ export abstract class GraphqlQueryBaseResolverService< await this.validate(args, options); - const dataSource = - await this.twentyORMGlobalManager.getDataSourceForWorkspace( - authContext.workspace.id, - ); + const workspaceDataSource = + await this.twentyORMGlobalManager.getDataSourceForWorkspace({ + workspaceId: authContext.workspace.id, + shouldFailIfMetadataNotFound: false, + }); - const featureFlagsMap = dataSource.featureFlagMap; + const featureFlagsMap = workspaceDataSource.featureFlagMap; const isPermissionsV2Enabled = featureFlagsMap[FeatureFlagKey.IsPermissionsV2Enabled]; @@ -127,7 +128,7 @@ export abstract class GraphqlQueryBaseResolverService< const executedByApiKey = isDefined(authContext.apiKey); const shouldBypassPermissionChecks = executedByApiKey; - const repository = dataSource.getRepository( + const repository = workspaceDataSource.getRepository( objectMetadataItemWithFieldMaps.nameSingular, shouldBypassPermissionChecks, roleId, @@ -151,7 +152,7 @@ export abstract class GraphqlQueryBaseResolverService< const graphqlQueryResolverExecutionArgs = { args: computedArgs, options, - dataSource, + dataSource: workspaceDataSource, repository, graphqlQueryParser, graphqlQuerySelectedFieldsResult, diff --git a/packages/twenty-server/src/engine/api/rest/core/rest-api-core-v2.service.ts b/packages/twenty-server/src/engine/api/rest/core/rest-api-core-v2.service.ts index db1e6be92..9a4d9f4aa 100644 --- a/packages/twenty-server/src/engine/api/rest/core/rest-api-core-v2.service.ts +++ b/packages/twenty-server/src/engine/api/rest/core/rest-api-core-v2.service.ts @@ -156,7 +156,10 @@ export class RestApiCoreServiceV2 { } const dataSource = - await this.twentyORMGlobalManager.getDataSourceForWorkspace(workspace.id); + await this.twentyORMGlobalManager.getDataSourceForWorkspace({ + workspaceId: workspace.id, + shouldFailIfMetadataNotFound: false, + }); const objectMetadataNameSingular = objectMetadata.objectMetadataMapItem.nameSingular; diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts index df25f71c3..74675a75c 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts @@ -100,7 +100,9 @@ export class GoogleAPIsService { ); const workspaceDataSource = - await this.twentyORMGlobalManager.getDataSourceForWorkspace(workspaceId); + await this.twentyORMGlobalManager.getDataSourceForWorkspace({ + workspaceId, + }); const scopes = getGoogleApisOauthScopes(); diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/microsoft-apis.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/microsoft-apis.service.ts index 54a8c3404..abda6711d 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/microsoft-apis.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/microsoft-apis.service.ts @@ -104,7 +104,9 @@ export class MicrosoftAPIsService { ); const workspaceDataSource = - await this.twentyORMGlobalManager.getDataSourceForWorkspace(workspaceId); + await this.twentyORMGlobalManager.getDataSourceForWorkspace({ + workspaceId, + }); const scopes = getMicrosoftApisOauthScopes(); diff --git a/packages/twenty-server/src/engine/core-modules/auth/strategies/jwt.auth.strategy.spec.ts b/packages/twenty-server/src/engine/core-modules/auth/strategies/jwt.auth.strategy.spec.ts index a4ff56d5c..e771641d9 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/strategies/jwt.auth.strategy.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/strategies/jwt.auth.strategy.spec.ts @@ -14,9 +14,7 @@ describe('JwtAuthStrategy', () => { let workspaceRepository: any; let userWorkspaceRepository: any; let userRepository: any; - let dataSourceService: any; - let typeORMService: any; - + let twentyORMGlobalManager: any; const jwt = { sub: 'sub-default', jti: 'jti-default', @@ -38,6 +36,12 @@ describe('JwtAuthStrategy', () => { extractJwtFromRequest: jest.fn(() => () => 'token'), }; + twentyORMGlobalManager = { + getRepositoryForWorkspace: jest.fn(async () => ({ + findOne: jest.fn(async () => ({ id: 'api-key-id', revokedAt: null })), + })), + }; + // first we test the API_KEY case it('should throw AuthException if type is API_KEY and workspace is not found', async () => { const payload = { @@ -50,10 +54,8 @@ describe('JwtAuthStrategy', () => { }; strategy = new JwtAuthStrategy( - {} as any, jwtWrapperService, - typeORMService, - dataSourceService, + twentyORMGlobalManager, workspaceRepository, {} as any, userWorkspaceRepository, @@ -77,19 +79,15 @@ describe('JwtAuthStrategy', () => { findOneBy: jest.fn(async () => new Workspace()), }; - dataSourceService = { - getLastDataSourceMetadataFromWorkspaceIdOrFail: jest.fn(async () => ({})), - }; - - typeORMService = { - connectToDataSource: jest.fn(async () => {}), + twentyORMGlobalManager = { + getRepositoryForWorkspace: jest.fn(async () => ({ + findOne: jest.fn(async () => null), + })), }; strategy = new JwtAuthStrategy( - {} as any, jwtWrapperService, - typeORMService, - dataSourceService, + twentyORMGlobalManager, workspaceRepository, {} as any, userWorkspaceRepository, @@ -113,21 +111,15 @@ describe('JwtAuthStrategy', () => { findOneBy: jest.fn(async () => new Workspace()), }; - const mockDataSource = { - query: jest - .fn() - .mockResolvedValue([{ id: 'api-key-id', revokedAt: null }]), + twentyORMGlobalManager = { + getRepositoryForWorkspace: jest.fn(async () => ({ + findOne: jest.fn(async () => ({ id: 'api-key-id', revokedAt: null })), + })), }; - jest - .spyOn(typeORMService, 'connectToDataSource') - .mockResolvedValue(mockDataSource as any); - strategy = new JwtAuthStrategy( - {} as any, jwtWrapperService, - typeORMService, - dataSourceService, + twentyORMGlobalManager, workspaceRepository, {} as any, userWorkspaceRepository, @@ -140,7 +132,6 @@ describe('JwtAuthStrategy', () => { }); // second we test the ACCESS cases - it('should throw AuthExceptionCode if type is ACCESS, no jti, and user not found', async () => { const payload = { sub: 'sub-default', @@ -156,10 +147,8 @@ describe('JwtAuthStrategy', () => { }; strategy = new JwtAuthStrategy( - {} as any, jwtWrapperService, - typeORMService, - dataSourceService, + twentyORMGlobalManager, workspaceRepository, userRepository, userWorkspaceRepository, @@ -194,10 +183,8 @@ describe('JwtAuthStrategy', () => { }; strategy = new JwtAuthStrategy( - {} as any, jwtWrapperService, - typeORMService, - dataSourceService, + twentyORMGlobalManager, workspaceRepository, userRepository, userWorkspaceRepository, @@ -235,10 +222,8 @@ describe('JwtAuthStrategy', () => { }; strategy = new JwtAuthStrategy( - {} as any, jwtWrapperService, - typeORMService, - dataSourceService, + twentyORMGlobalManager, workspaceRepository, userRepository, userWorkspaceRepository, diff --git a/packages/twenty-server/src/engine/core-modules/auth/strategies/jwt.auth.strategy.ts b/packages/twenty-server/src/engine/core-modules/auth/strategies/jwt.auth.strategy.ts index 012e44832..65f4cf241 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/strategies/jwt.auth.strategy.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/strategies/jwt.auth.strategy.ts @@ -5,7 +5,6 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Strategy } from 'passport-jwt'; import { Repository } from 'typeorm'; -import { TypeORMService } from 'src/database/typeorm/typeorm.service'; import { AuthException, AuthExceptionCode, @@ -15,20 +14,17 @@ import { JwtPayload, } from 'src/engine/core-modules/auth/types/auth-context.type'; import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service'; -import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { User } from 'src/engine/core-modules/user/user.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { ApiKeyWorkspaceEntity } from 'src/modules/api-key/standard-objects/api-key.workspace-entity'; @Injectable() export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') { constructor( - private readonly twentyConfigService: TwentyConfigService, private readonly jwtWrapperService: JwtWrapperService, - private readonly typeORMService: TypeORMService, - private readonly dataSourceService: DataSourceService, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, @InjectRepository(Workspace, 'core') private readonly workspaceRepository: Repository, @InjectRepository(User, 'core') @@ -37,7 +33,7 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') { private readonly userWorkspaceRepository: Repository, ) { const jwtFromRequestFunction = jwtWrapperService.extractJwtFromRequest(); - const secretOrKeyProviderFunction = async (request, rawJwtToken, done) => { + const secretOrKeyProviderFunction = async (_request, rawJwtToken, done) => { try { const decodedToken = jwtWrapperService.decode( rawJwtToken, @@ -75,20 +71,20 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') { ); } - const dataSourceMetadata = - await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail( + const apiKeyRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( workspace.id, + 'apiKey', + { + shouldBypassPermissionChecks: true, + }, ); - const workspaceDataSource = - await this.typeORMService.connectToDataSource(dataSourceMetadata); - - const res = await workspaceDataSource?.query( - `SELECT * FROM ${dataSourceMetadata.schema}."apiKey" WHERE id = $1`, - [payload.jti], - ); - - apiKey = res?.[0]; + apiKey = await apiKeyRepository.findOne({ + where: { + id: payload.jti, + }, + }); if (!apiKey || apiKey.revokedAt) { throw new AuthException( diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.spec.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.spec.ts index aff325701..b30d0a710 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { DataSource, Repository } from 'typeorm'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; @@ -13,7 +13,6 @@ import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/use import { User } from 'src/engine/core-modules/user/user.entity'; import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { PermissionsException } from 'src/engine/metadata-modules/permissions/permissions.exception'; @@ -27,7 +26,6 @@ describe('UserWorkspaceService', () => { let userWorkspaceRepository: Repository; let userRepository: Repository; let objectMetadataRepository: Repository; - let dataSourceService: DataSourceService; let typeORMService: TypeORMService; let workspaceInvitationService: WorkspaceInvitationService; let workspaceEventEmitter: WorkspaceEventEmitter; @@ -71,7 +69,7 @@ describe('UserWorkspaceService', () => { { provide: TypeORMService, useValue: { - connectToDataSource: jest.fn(), + getMainDataSource: jest.fn(), }, }, { @@ -116,7 +114,6 @@ describe('UserWorkspaceService', () => { objectMetadataRepository = module.get( getRepositoryToken(ObjectMetadataEntity, 'metadata'), ); - dataSourceService = module.get(DataSourceService); typeORMService = module.get(TypeORMService); workspaceInvitationService = module.get( WorkspaceInvitationService, @@ -179,12 +176,9 @@ describe('UserWorkspaceService', () => { defaultAvatarUrl: 'avatar-url', locale: 'en', } as User; - const dataSourceMetadata = { - schema: 'public', - } as DataSourceEntity; - const workspaceDataSource = { + const mainDataSource = { query: jest.fn(), - }; + } as unknown as DataSource; const workspaceMember = [ { id: 'workspace-member-id', @@ -197,17 +191,16 @@ describe('UserWorkspaceService', () => { const objectMetadata = { nameSingular: 'workspaceMember', } as ObjectMetadataEntity; + const workspaceMemberRepository = { + insert: jest.fn(), + find: jest.fn().mockResolvedValue(workspaceMember), + }; jest - .spyOn( - dataSourceService, - 'getLastDataSourceMetadataFromWorkspaceIdOrFail', - ) - .mockResolvedValue(dataSourceMetadata); + .spyOn(typeORMService, 'getMainDataSource') + .mockReturnValue(mainDataSource); jest - .spyOn(typeORMService, 'connectToDataSource') - .mockResolvedValue(workspaceDataSource as any); - workspaceDataSource.query + .spyOn(mainDataSource, 'query') .mockResolvedValueOnce(undefined) .mockResolvedValueOnce(workspaceMember); jest @@ -217,15 +210,23 @@ describe('UserWorkspaceService', () => { .spyOn(workspaceEventEmitter, 'emitDatabaseBatchEvent') .mockImplementation(); + jest + .spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace') + .mockResolvedValue(workspaceMemberRepository as any); + await service.createWorkspaceMember(workspaceId, user); - expect( - dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail, - ).toHaveBeenCalledWith(workspaceId); - expect(typeORMService.connectToDataSource).toHaveBeenCalledWith( - dataSourceMetadata, - ); - expect(workspaceDataSource.query).toHaveBeenCalledTimes(2); + expect(workspaceMemberRepository.insert).toHaveBeenCalledWith({ + name: { + firstName: user.firstName, + lastName: user.lastName, + }, + colorScheme: 'System', + userId: user.id, + userEmail: user.email, + locale: 'en', + avatarUrl: 'avatar-url', + }); expect(objectMetadataRepository.findOneOrFail).toHaveBeenCalledWith({ where: { nameSingular: 'workspaceMember', diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts index 44f625e14..f88c56bbd 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts @@ -2,7 +2,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; -import { SOURCE_LOCALE } from 'twenty-shared/translations'; +import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations'; import { isDefined } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; @@ -69,33 +69,35 @@ export class UserWorkspaceService extends TypeOrmQueryService { } async createWorkspaceMember(workspaceId: string, user: User) { - const dataSourceMetadata = - await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail( + const workspaceMemberRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( workspaceId, + 'workspaceMember', + { + shouldBypassPermissionChecks: true, + }, ); - const workspaceDataSource = - await this.typeORMService.connectToDataSource(dataSourceMetadata); + await workspaceMemberRepository.insert({ + name: { + firstName: user.firstName, + lastName: user.lastName, + }, + colorScheme: 'System', + userId: user.id, + userEmail: user.email, + avatarUrl: user.defaultAvatarUrl ?? '', + locale: (user.locale ?? SOURCE_LOCALE) as keyof typeof APP_LOCALES, + }); - await workspaceDataSource?.query( - `INSERT INTO ${dataSourceMetadata.schema}."workspaceMember" - ("nameFirstName", "nameLastName", "colorScheme", "userId", "userEmail", "avatarUrl", "locale") - VALUES ($1, $2, 'System', $3, $4, $5, $6)`, - [ - user.firstName, - user.lastName, - user.id, - user.email, - user.defaultAvatarUrl ?? '', - user.locale ?? SOURCE_LOCALE, - ], - ); - const workspaceMember = await workspaceDataSource?.query( - `SELECT * FROM ${dataSourceMetadata.schema}."workspaceMember" WHERE "userId"='${user.id}'`, - ); + const workspaceMember = await workspaceMemberRepository.find({ + where: { + userId: user.id, + }, + }); assert( - workspaceMember.length === 1, + workspaceMember?.length === 1, `Error while creating workspace member ${user.email} on workspace ${workspaceId}`, ); const objectMetadata = await this.objectMetadataRepository.findOneOrFail({ diff --git a/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts b/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts index cae4ba870..8c248e4cc 100644 --- a/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts +++ b/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts @@ -6,13 +6,11 @@ import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace'; import { Repository } from 'typeorm'; -import { TypeORMService } from 'src/database/typeorm/typeorm.service'; import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; import { AuthException, AuthExceptionCode, } from 'src/engine/core-modules/auth/auth.exception'; -import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; import { User } from 'src/engine/core-modules/user/user.entity'; import { userValidator } from 'src/engine/core-modules/user/user.validate'; @@ -38,13 +36,11 @@ export class UserService extends TypeOrmQueryService { @InjectRepository(ObjectMetadataEntity, 'metadata') private readonly objectMetadataRepository: Repository, private readonly dataSourceService: DataSourceService, - private readonly typeORMService: TypeORMService, private readonly workspaceEventEmitter: WorkspaceEventEmitter, private readonly workspaceService: WorkspaceService, private readonly twentyORMGlobalManager: TwentyORMGlobalManager, private readonly userRoleService: UserRoleService, private readonly userWorkspaceService: UserWorkspaceService, - private readonly featureFlagService: FeatureFlagService, ) { super(userRepository); } @@ -88,17 +84,16 @@ export class UserService extends TypeOrmQueryService { userId: string; workspaceId: string; }) { - const dataSourceMetadata = - await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail( + const workspaceMemberRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( workspaceId, + 'workspaceMember', + { + shouldBypassPermissionChecks: true, + }, ); - const workspaceDataSource = - await this.typeORMService.connectToDataSource(dataSourceMetadata); - - const workspaceMembers = await workspaceDataSource?.query( - `SELECT * FROM ${dataSourceMetadata.schema}."workspaceMember"`, - ); + const workspaceMembers = await workspaceMemberRepository.find(); if (workspaceMembers.length > 1) { const userWorkspace = @@ -119,9 +114,7 @@ export class UserService extends TypeOrmQueryService { assert(workspaceMember, 'WorkspaceMember not found'); - await workspaceDataSource?.query( - `DELETE FROM ${dataSourceMetadata.schema}."workspaceMember" WHERE "userId" = '${userId}'`, - ); + await workspaceMemberRepository.delete({ userId }); const objectMetadata = await this.objectMetadataRepository.findOneOrFail({ where: { diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts index 8aebca6a3..c3967a10b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts @@ -61,11 +61,11 @@ import { } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory'; import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service'; +import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; import { ViewService } from 'src/modules/view/services/view.service'; -import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity'; import { FieldMetadataValidationService } from './field-metadata-validation.service'; import { FieldMetadataEntity } from './field-metadata.entity'; @@ -910,74 +910,70 @@ export class FieldMetadataService extends TypeOrmQueryService { + const viewsRepository = workspaceEntityManager.getRepository('view', { + shouldBypassPermissionChecks: true, + }); - if (!workspaceQueryRunner) { - throw new FieldMetadataException( - 'Could not create workspace query runner', - FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR, - ); - } - - await workspaceQueryRunner.connect(); - await workspaceQueryRunner.startTransaction(); - - try { - for (const createdFieldMetadata of createdFieldMetadatas) { - const view = await workspaceQueryRunner?.query( - `SELECT id FROM ${dataSourceMetadata.schema}."view" - WHERE "objectMetadataId" = '${createdFieldMetadata.objectMetadataId}'`, + const viewFieldsRepository = workspaceEntityManager.getRepository( + 'viewField', + { + shouldBypassPermissionChecks: true, + }, ); - if (!isEmpty(view)) { - const existingViewFields = (await workspaceQueryRunner?.query( - `SELECT * FROM ${dataSourceMetadata.schema}."viewField" - WHERE "viewId" = '${view[0].id}'`, - )) as ViewFieldWorkspaceEntity[]; - const isVisible = - existingViewFields.length < settings.maxVisibleViewFields; + for (const createdFieldMetadata of createdFieldMetadatas) { + const views = await viewsRepository.find({ + where: { + objectMetadataId: createdFieldMetadata.objectMetadataId, + }, + }); - const createdFieldIsAlreadyInView = existingViewFields.some( - (existingViewField) => - existingViewField.fieldMetadataId === createdFieldMetadata.id, - ); + if (!isEmpty(views)) { + const view = views[0]; + const existingViewFields = await viewFieldsRepository.find({ + where: { + viewId: view.id, + }, + }); - if (!createdFieldIsAlreadyInView) { - const lastPosition = existingViewFields - .map((viewField) => viewField.position) - .reduce((acc, position) => { - if (position > acc) { - return position; - } + const isVisible = + existingViewFields.length < settings.maxVisibleViewFields; - return acc; - }, -1); - - await workspaceQueryRunner?.query( - `INSERT INTO ${dataSourceMetadata.schema}."viewField" - ("fieldMetadataId", "position", "isVisible", "size", "viewId") - VALUES ('${createdFieldMetadata.id}', '${ - lastPosition + 1 - }', ${isVisible}, 180, '${view[0].id}')`, + const createdFieldIsAlreadyInView = existingViewFields.some( + (existingViewField) => + existingViewField.fieldMetadataId === createdFieldMetadata.id, ); + + if (!createdFieldIsAlreadyInView) { + const lastPosition = existingViewFields + .map((viewField) => viewField.position) + .reduce((acc, position) => { + if (position > acc) { + return position; + } + + return acc; + }, -1); + + await viewFieldsRepository.insert({ + fieldMetadataId: createdFieldMetadata.id, + position: lastPosition + 1, + isVisible, + size: 180, + viewId: view.id, + }); + } } } - } - await workspaceQueryRunner.commitTransaction(); - } catch (error) { - await workspaceQueryRunner.rollbackTransaction(); - throw error; - } finally { - await workspaceQueryRunner.release(); - } + }, + ); } async getFieldMetadataItemsByBatch( diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/distant-table/distant-table.service.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/distant-table/distant-table.service.ts index 337ab25be..61240d751 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/distant-table/distant-table.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/distant-table/distant-table.service.ts @@ -8,15 +8,15 @@ import { RemoteServerEntity, RemoteServerType, } from 'src/engine/metadata-modules/remote-server/remote-server.entity'; -import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; -import { DistantTables } from 'src/engine/metadata-modules/remote-server/remote-table/distant-table/types/distant-table'; -import { STRIPE_DISTANT_TABLES } from 'src/engine/metadata-modules/remote-server/remote-table/distant-table/utils/stripe-distant-tables.util'; -import { PostgresTableSchemaColumn } from 'src/engine/metadata-modules/remote-server/types/postgres-table-schema-column'; -import { isQueryTimeoutError } from 'src/engine/utils/query-timeout.util'; import { DistantTableException, DistantTableExceptionCode, } from 'src/engine/metadata-modules/remote-server/remote-table/distant-table/distant-table.exception'; +import { DistantTables } from 'src/engine/metadata-modules/remote-server/remote-table/distant-table/types/distant-table'; +import { STRIPE_DISTANT_TABLES } from 'src/engine/metadata-modules/remote-server/remote-table/distant-table/utils/stripe-distant-tables.util'; +import { PostgresTableSchemaColumn } from 'src/engine/metadata-modules/remote-server/types/postgres-table-schema-column'; +import { isQueryTimeoutError } from 'src/engine/utils/query-timeout.util'; +import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; @Injectable() export class DistantTableService { @@ -73,13 +73,11 @@ export class DistantTableService { const tmpSchemaId = v4(); const tmpSchemaName = `${workspaceId}_${remoteServer.id}_${tmpSchemaId}`; - const workspaceDataSource = - await this.workspaceDataSourceService.connectToWorkspaceDataSource( - workspaceId, - ); + const mainDataSource = + await this.workspaceDataSourceService.connectToMainDataSource(); try { - const distantTables = await workspaceDataSource.transaction( + const distantTables = await mainDataSource.transaction( async (entityManager: EntityManager) => { await entityManager.query(`CREATE SCHEMA "${tmpSchemaName}"`); diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/foreign-table/foreign-table.service.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/foreign-table/foreign-table.service.ts index 55e545ed8..fc573998c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/foreign-table/foreign-table.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/foreign-table/foreign-table.service.ts @@ -37,13 +37,11 @@ export class ForeignTableService { workspaceId: string, foreignDataWrapperId: string, ): Promise { - const workspaceDataSource = - await this.workspaceDataSourceService.connectToWorkspaceDataSource( - workspaceId, - ); + const mainDataSource = + await this.workspaceDataSourceService.connectToMainDataSource(); return ( - await workspaceDataSource.query( + await mainDataSource.query( `SELECT foreign_table_name, foreign_server_name FROM information_schema.foreign_tables WHERE foreign_server_name = $1`, [foreignDataWrapperId], ) diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.service.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.service.ts index d7d3b985a..b8da05ee4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.service.ts @@ -182,16 +182,14 @@ export class RemoteTableService { workspaceId, ); - const workspaceDataSource = - await this.workspaceDataSourceService.connectToWorkspaceDataSource( - workspaceId, - ); + const mainDataSource = + await this.workspaceDataSourceService.connectToMainDataSource(); const { baseName: localTableBaseName, suffix: localTableSuffix } = await getRemoteTableLocalName( input.name, dataSourceMetatada.schema, - workspaceDataSource, + mainDataSource, ); const localTableName = localTableSuffix diff --git a/packages/twenty-server/src/engine/seeder/seeder.service.ts b/packages/twenty-server/src/engine/seeder/seeder.service.ts index 1589f7d93..c4a7eea35 100644 --- a/packages/twenty-server/src/engine/seeder/seeder.service.ts +++ b/packages/twenty-server/src/engine/seeder/seeder.service.ts @@ -61,13 +61,10 @@ export class SeederService { const schemaName = this.workspaceDataSourceService.getSchemaName(workspaceId); - const workspaceDataSource: DataSource = - await this.workspaceDataSourceService.connectToWorkspaceDataSource( - workspaceId, - ); + const mainDataSource: DataSource = + await this.workspaceDataSourceService.connectToMainDataSource(); - const entityManager: EntityManager = - workspaceDataSource.createEntityManager(); + const entityManager: EntityManager = mainDataSource.createEntityManager(); const filteredFieldMetadataSeeds = objectMetadataSeed.fields.filter( (field) => diff --git a/packages/twenty-server/src/engine/twenty-orm/datasource/workspace.datasource.ts b/packages/twenty-server/src/engine/twenty-orm/datasource/workspace.datasource.ts index c2aeb968a..922b776a7 100644 --- a/packages/twenty-server/src/engine/twenty-orm/datasource/workspace.datasource.ts +++ b/packages/twenty-server/src/engine/twenty-orm/datasource/workspace.datasource.ts @@ -47,15 +47,15 @@ export class WorkspaceDataSource extends DataSource { roleId?: string, ): WorkspaceRepository { if (shouldBypassPermissionChecks === true) { - return this.manager.getRepository(target, shouldBypassPermissionChecks); + return this.manager.getRepository(target, { + shouldBypassPermissionChecks: true, + }); } if (roleId) { - return this.manager.getRepository( - target, - shouldBypassPermissionChecks, + return this.manager.getRepository(target, { roleId, - ); + }); } return this.manager.getRepository(target); diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts index 9d8279eea..0eb037918 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts @@ -2,6 +2,7 @@ import { ObjectRecordsPermissions } from 'twenty-shared/types'; import { EntityManager, EntityTarget, + FindManyOptions, InsertResult, ObjectLiteral, QueryRunner, @@ -44,16 +45,19 @@ export class WorkspaceEntityManager extends EntityManager { override getRepository( target: EntityTarget, - shouldBypassPermissionChecks = false, - roleId?: string, + permissionOptions?: { + shouldBypassPermissionChecks?: boolean; + roleId?: string; + }, ): WorkspaceRepository { const dataSource = this.connection; const repositoryKey = this.getRepositoryKey({ target, dataSource, - roleId, - shouldBypassPermissionChecks, + roleId: permissionOptions?.roleId, + shouldBypassPermissionChecks: + permissionOptions?.shouldBypassPermissionChecks ?? false, }); const repoFromMap = this.repositories.get(repositoryKey); @@ -63,10 +67,11 @@ export class WorkspaceEntityManager extends EntityManager { let objectPermissions = {}; - if (roleId) { + if (permissionOptions?.roleId) { const objectPermissionsByRoleId = dataSource.permissionsPerRoleId; - objectPermissions = objectPermissionsByRoleId?.[roleId] ?? {}; + objectPermissions = + objectPermissionsByRoleId?.[permissionOptions?.roleId] ?? {}; } const newRepository = new WorkspaceRepository( @@ -76,7 +81,7 @@ export class WorkspaceEntityManager extends EntityManager { dataSource.featureFlagMap, this.queryRunner, objectPermissions, - shouldBypassPermissionChecks, + permissionOptions?.shouldBypassPermissionChecks, ); this.repositories.set(repositoryKey, newRepository); @@ -135,17 +140,30 @@ export class WorkspaceEntityManager extends EntityManager { } } + override find( + target: EntityTarget, + options?: FindManyOptions, + permissionOptions?: { + shouldBypassPermissionChecks?: boolean; + objectRecordsPermissions?: ObjectRecordsPermissions; + }, + ): Promise { + this.validatePermissions(target, 'select', permissionOptions); + + return super.find(target, options); + } + override insert( target: EntityTarget, entityOrEntities: | QueryDeepPartialEntity | QueryDeepPartialEntity[], - options?: { + permissionOptions?: { shouldBypassPermissionChecks?: boolean; objectRecordsPermissions?: ObjectRecordsPermissions; }, ): Promise { - this.validatePermissions(target, 'insert', options); + this.validatePermissions(target, 'insert', permissionOptions); return super.insert(target, entityOrEntities); } @@ -156,12 +174,12 @@ export class WorkspaceEntityManager extends EntityManager { | QueryDeepPartialEntity | QueryDeepPartialEntity[], conflictPathsOrOptions: string[] | UpsertOptions, - options?: { + permissionOptions?: { shouldBypassPermissionChecks?: boolean; objectRecordsPermissions?: ObjectRecordsPermissions; }, ): Promise { - this.validatePermissions(target, 'update', options); + this.validatePermissions(target, 'update', permissionOptions); return super.upsert(target, entityOrEntities, conflictPathsOrOptions); } @@ -194,7 +212,7 @@ export class WorkspaceEntityManager extends EntityManager { private validatePermissions( target: EntityTarget, operationType: OperationType, - options?: { + permissionOptions?: { shouldBypassPermissionChecks?: boolean; objectRecordsPermissions?: ObjectRecordsPermissions; }, @@ -208,14 +226,15 @@ export class WorkspaceEntityManager extends EntityManager { return; } - if (options?.shouldBypassPermissionChecks === true) { + if (permissionOptions?.shouldBypassPermissionChecks === true) { return; } validateOperationIsPermittedOrThrow({ entityName: this.extractTargetNameSingularFromEntityTarget(target), operationType, - objectRecordsPermissions: options?.objectRecordsPermissions ?? {}, + objectRecordsPermissions: + permissionOptions?.objectRecordsPermissions ?? {}, objectMetadataMaps: this.internalContext.objectMetadataMaps, }); } diff --git a/packages/twenty-server/src/engine/twenty-orm/twenty-orm-global.manager.ts b/packages/twenty-server/src/engine/twenty-orm/twenty-orm-global.manager.ts index 1a6b485eb..340eb1c0a 100644 --- a/packages/twenty-server/src/engine/twenty-orm/twenty-orm-global.manager.ts +++ b/packages/twenty-server/src/engine/twenty-orm/twenty-orm-global.manager.ts @@ -65,10 +65,13 @@ export class TwentyORMGlobalManager { return repository; } - async getDataSourceForWorkspace( - workspaceId: string, + async getDataSourceForWorkspace({ + workspaceId, shouldFailIfMetadataNotFound = true, - ) { + }: { + workspaceId: string; + shouldFailIfMetadataNotFound?: boolean; + }) { return await this.workspaceDataSourceFactory.create( workspaceId, null, diff --git a/packages/twenty-server/src/engine/workspace-cache-storage/workspace-cache-storage.service.ts b/packages/twenty-server/src/engine/workspace-cache-storage/workspace-cache-storage.service.ts index e996c1ab3..e8592d71d 100644 --- a/packages/twenty-server/src/engine/workspace-cache-storage/workspace-cache-storage.service.ts +++ b/packages/twenty-server/src/engine/workspace-cache-storage/workspace-cache-storage.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common'; import crypto from 'crypto'; +import { isDefined } from 'twenty-shared/utils'; import { EntitySchemaOptions } from 'typeorm'; import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface'; @@ -277,8 +278,13 @@ export class WorkspaceCacheStorageService { ); } - async flush(workspaceId: string, metadataVersion: number): Promise { - await this.flushVersionedMetadata(workspaceId, metadataVersion); + async flush( + workspaceId: string, + metadataVersion: number | undefined, + ): Promise { + if (isDefined(metadataVersion)) { + await this.flushVersionedMetadata(workspaceId, metadataVersion); + } await this.cacheStorageService.del( `${WorkspaceCacheKeys.MetadataPermissionsRolesPermissions}:${workspaceId}`, 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 6b4e87d64..11c597253 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 @@ -3,7 +3,6 @@ import { Injectable } from '@nestjs/common'; import { DataSource, EntityManager } from 'typeorm'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; -import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; @Injectable() @@ -20,11 +19,12 @@ export class WorkspaceDataSourceService { * @param workspaceId * @returns */ - public async connectToWorkspaceDataSource( - workspaceId: string, - ): Promise { - const { dataSource } = - await this.connectedToWorkspaceDataSourceAndReturnMetadata(workspaceId); + public async connectToMainDataSource(): Promise { + const dataSource = this.typeormService.getMainDataSource(); + + if (!dataSource) { + throw new Error(`Could not connect to workspace data source`); + } return dataSource; } @@ -38,26 +38,6 @@ export class WorkspaceDataSourceService { return dataSource.length > 0; } - public async connectedToWorkspaceDataSourceAndReturnMetadata( - workspaceId: string, - ): Promise<{ dataSource: DataSource; dataSourceMetadata: DataSourceEntity }> { - const dataSourceMetadata = - await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail( - workspaceId, - ); - - const dataSource = - await this.typeormService.connectToDataSource(dataSourceMetadata); - - if (!dataSource) { - throw new Error( - `Could not connect to workspace data source for workspace ${workspaceId}`, - ); - } - - return { dataSource, dataSourceMetadata }; - } - /** * * Create a new DB schema for a workspace @@ -128,10 +108,9 @@ export class WorkspaceDataSourceService { if (transactionManager) { return await transactionManager.query(query, parameters); } - const workspaceDataSource = - await this.connectToWorkspaceDataSource(workspaceId); + const dataSource = await this.connectToMainDataSource(); - return await workspaceDataSource.query(query, parameters); + return await dataSource.query(query, parameters); } catch (error) { throw new Error( `Error executing raw query for workspace ${workspaceId}: ${error.message}`, diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/standard-objects-prefill-data.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/standard-objects-prefill-data.ts index cbd636d14..d39d70358 100644 --- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/standard-objects-prefill-data.ts +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/standard-objects-prefill-data.ts @@ -9,7 +9,7 @@ import { personPrefillData } from 'src/engine/workspace-manager/standard-objects import { seedViewWithDemoData } from 'src/engine/workspace-manager/standard-objects-prefill-data/seed-view-with-demo-data'; export const standardObjectsPrefillData = async ( - workspaceDataSource: DataSource, + mainDataSource: DataSource, schemaName: string, objectMetadata: ObjectMetadataEntity[], ) => { @@ -34,30 +34,28 @@ export const standardObjectsPrefillData = async ( return acc; }, {}); - workspaceDataSource.transaction( - async (entityManager: WorkspaceEntityManager) => { - await companyPrefillData(entityManager, schemaName); - await personPrefillData(entityManager, schemaName); - const viewDefinitionsWithId = await seedViewWithDemoData( - entityManager, - schemaName, - objectMetadataMap, - ); + mainDataSource.transaction(async (entityManager: WorkspaceEntityManager) => { + await companyPrefillData(entityManager, schemaName); + await personPrefillData(entityManager, schemaName); + const viewDefinitionsWithId = await seedViewWithDemoData( + entityManager, + schemaName, + objectMetadataMap, + ); - await seedWorkspaceFavorites( - viewDefinitionsWithId - .filter( - (view) => - view.key === 'INDEX' && - shouldSeedWorkspaceFavorite( - view.objectMetadataId, - objectMetadataMap, - ), - ) - .map((view) => view.id), - entityManager, - schemaName, - ); - }, - ); + await seedWorkspaceFavorites( + viewDefinitionsWithId + .filter( + (view) => + view.key === 'INDEX' && + shouldSeedWorkspaceFavorite( + view.objectMetadataId, + objectMetadataMap, + ), + ) + .map((view) => view.id), + entityManager, + schemaName, + ); + }); }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/workspace-health.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/workspace-health.service.ts index 0b8bdf7cf..1d7ef6bd8 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/workspace-health.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-health/workspace-health.service.ts @@ -10,7 +10,6 @@ import { WorkspaceHealthOptions, } from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-options.interface'; -import { TypeORMService } from 'src/database/typeorm/typeorm.service'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; @@ -31,7 +30,6 @@ export class WorkspaceHealthService { @InjectDataSource('metadata') private readonly metadataDataSource: DataSource, private readonly dataSourceService: DataSourceService, - private readonly typeORMService: TypeORMService, private readonly objectMetadataService: ObjectMetadataService, private readonly databaseStructureService: DatabaseStructureService, private readonly workspaceDataSourceService: WorkspaceDataSourceService, @@ -62,9 +60,6 @@ export class WorkspaceHealthService { ); } - // Try to connect to the data source - await this.typeORMService.connectToDataSource(dataSourceMetadata); - const objectMetadataCollection = await this.objectMetadataService.findManyWithinWorkspace(workspaceId); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts index f30efe033..86e7f99c7 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts @@ -196,20 +196,18 @@ export class WorkspaceManagerService { dataSourceMetadata: DataSourceEntity, workspaceId: string, ) { - const workspaceDataSource = - await this.workspaceDataSourceService.connectToWorkspaceDataSource( - workspaceId, - ); + const mainDataSource = + await this.workspaceDataSourceService.connectToMainDataSource(); - if (!workspaceDataSource) { - throw new Error('Could not connect to workspace data source'); + if (!mainDataSource) { + throw new Error('Could not connect to main data source'); } const createdObjectMetadata = await this.objectMetadataService.findManyWithinWorkspace(workspaceId); await standardObjectsPrefillData( - workspaceDataSource, + mainDataSource, dataSourceMetadata.schema, createdObjectMetadata, ); @@ -226,20 +224,18 @@ export class WorkspaceManagerService { dataSourceMetadata: DataSourceEntity, workspaceId: string, ) { - const workspaceDataSource = - await this.workspaceDataSourceService.connectToWorkspaceDataSource( - workspaceId, - ); + const mainDataSource = + await this.workspaceDataSourceService.connectToMainDataSource(); - if (!workspaceDataSource) { - throw new Error('Could not connect to workspace data source'); + if (!mainDataSource) { + throw new Error('Could not connect to main data source'); } const createdObjectMetadata = await this.objectMetadataService.findManyWithinWorkspace(workspaceId); await seedWorkspaceWithDemoData( - workspaceDataSource, + mainDataSource, dataSourceMetadata.schema, createdObjectMetadata, ); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service.ts index 814c2f374..4405eb801 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service.ts @@ -55,13 +55,11 @@ export class WorkspaceMigrationRunnerService { public async executeMigrationFromPendingMigrations( workspaceId: string, ): Promise { - const workspaceDataSource = - await this.workspaceDataSourceService.connectToWorkspaceDataSource( - workspaceId, - ); + const mainDataSource = + await this.workspaceDataSourceService.connectToMainDataSource(); - if (!workspaceDataSource) { - throw new Error('Workspace data source not found'); + if (!mainDataSource) { + throw new Error('Main data source not found'); } const pendingMigrations = @@ -76,7 +74,7 @@ export class WorkspaceMigrationRunnerService { return [...acc, ...pendingMigration.migrations]; }, []); - const queryRunner = workspaceDataSource?.createQueryRunner(); + const queryRunner = mainDataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-records-permissions/destroy-one-object-records-permissions.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-records-permissions/destroy-one-object-records-permissions.integration-spec.ts index 02b7336c6..3bfb818f3 100644 --- a/packages/twenty-server/test/integration/graphql/suites/object-records-permissions/destroy-one-object-records-permissions.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-records-permissions/destroy-one-object-records-permissions.integration-spec.ts @@ -28,7 +28,6 @@ describe('destroyOneObjectRecordsPermissions', () => { }); it('should throw a permission error when user does not have permission (guest role)', async () => { - const personId = randomUUID(); const graphqlOperation = destroyOneOperationFactory({ objectMetadataSingularName: 'person', gqlFields: PERSON_GQL_FIELDS,