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
This commit is contained in:
Marie
2025-05-07 10:42:51 +02:00
committed by GitHub
parent b5bacbbd29
commit 463dee3fe6
33 changed files with 324 additions and 441 deletions

View File

@ -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,

View File

@ -46,7 +46,7 @@ export class DataSeedDemoWorkspaceService {
const appVersion = this.twentyConfigService.get('APP_VERSION');
await seedCoreSchema({
workspaceDataSource: rawDataSource,
dataSource: rawDataSource,
workspaceId,
appVersion,
seedBilling: false,

View File

@ -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(

View File

@ -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],
})

View File

@ -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`,

View File

@ -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: {

View File

@ -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);
}
};

View File

@ -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}`)

View File

@ -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<string, DataSource> = new Map();
private isDatasourceInitializing: Map<string, boolean> = 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<DataSource | undefined> {
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<DataSource> {
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<string> {
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();
}
}
}

View File

@ -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,

View File

@ -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;

View File

@ -100,7 +100,9 @@ export class GoogleAPIsService {
);
const workspaceDataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace(workspaceId);
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
workspaceId,
});
const scopes = getGoogleApisOauthScopes();

View File

@ -104,7 +104,9 @@ export class MicrosoftAPIsService {
);
const workspaceDataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace(workspaceId);
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
workspaceId,
});
const scopes = getMicrosoftApisOauthScopes();

View File

@ -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,

View File

@ -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<Workspace>,
@InjectRepository(User, 'core')
@ -37,7 +33,7 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
private readonly userWorkspaceRepository: Repository<UserWorkspace>,
) {
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<ApiKeyWorkspaceEntity>(
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(

View File

@ -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<UserWorkspace>;
let userRepository: Repository<User>;
let objectMetadataRepository: Repository<ObjectMetadataEntity>;
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>(DataSourceService);
typeORMService = module.get<TypeORMService>(TypeORMService);
workspaceInvitationService = module.get<WorkspaceInvitationService>(
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',

View File

@ -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<UserWorkspace> {
}
async createWorkspaceMember(workspaceId: string, user: User) {
const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
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({

View File

@ -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<User> {
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
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<User> {
userId: string;
workspaceId: string;
}) {
const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
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<User> {
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: {

View File

@ -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<FieldMetadataEntit
createdFieldMetadatas: FieldMetadataEntity[],
workspaceId: string,
) {
const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
workspaceId,
);
const workspaceDataSource =
await this.typeORMService.connectToDataSource(dataSourceMetadata);
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
workspaceId,
});
const workspaceQueryRunner = workspaceDataSource?.createQueryRunner();
await workspaceDataSource.transaction(
async (workspaceEntityManager: WorkspaceEntityManager) => {
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(

View File

@ -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}"`);

View File

@ -37,13 +37,11 @@ export class ForeignTableService {
workspaceId: string,
foreignDataWrapperId: string,
): Promise<string[]> {
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],
)

View File

@ -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

View File

@ -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) =>

View File

@ -47,15 +47,15 @@ export class WorkspaceDataSource extends DataSource {
roleId?: string,
): WorkspaceRepository<Entity> {
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);

View File

@ -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<Entity extends ObjectLiteral>(
target: EntityTarget<Entity>,
shouldBypassPermissionChecks = false,
roleId?: string,
permissionOptions?: {
shouldBypassPermissionChecks?: boolean;
roleId?: string;
},
): WorkspaceRepository<Entity> {
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<Entity>(
@ -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<Entity extends ObjectLiteral>(
target: EntityTarget<Entity>,
options?: FindManyOptions<Entity>,
permissionOptions?: {
shouldBypassPermissionChecks?: boolean;
objectRecordsPermissions?: ObjectRecordsPermissions;
},
): Promise<Entity[]> {
this.validatePermissions(target, 'select', permissionOptions);
return super.find(target, options);
}
override insert<Entity extends ObjectLiteral>(
target: EntityTarget<Entity>,
entityOrEntities:
| QueryDeepPartialEntity<Entity>
| QueryDeepPartialEntity<Entity>[],
options?: {
permissionOptions?: {
shouldBypassPermissionChecks?: boolean;
objectRecordsPermissions?: ObjectRecordsPermissions;
},
): Promise<InsertResult> {
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<Entity>
| QueryDeepPartialEntity<Entity>[],
conflictPathsOrOptions: string[] | UpsertOptions<Entity>,
options?: {
permissionOptions?: {
shouldBypassPermissionChecks?: boolean;
objectRecordsPermissions?: ObjectRecordsPermissions;
},
): Promise<InsertResult> {
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<Entity extends ObjectLiteral>(
target: EntityTarget<Entity>,
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,
});
}

View File

@ -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,

View File

@ -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<void> {
await this.flushVersionedMetadata(workspaceId, metadataVersion);
async flush(
workspaceId: string,
metadataVersion: number | undefined,
): Promise<void> {
if (isDefined(metadataVersion)) {
await this.flushVersionedMetadata(workspaceId, metadataVersion);
}
await this.cacheStorageService.del(
`${WorkspaceCacheKeys.MetadataPermissionsRolesPermissions}:${workspaceId}`,

View File

@ -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<DataSource> {
const { dataSource } =
await this.connectedToWorkspaceDataSourceAndReturnMetadata(workspaceId);
public async connectToMainDataSource(): Promise<DataSource> {
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}`,

View File

@ -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,
);
});
};

View File

@ -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);

View File

@ -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,
);

View File

@ -55,13 +55,11 @@ export class WorkspaceMigrationRunnerService {
public async executeMigrationFromPendingMigrations(
workspaceId: string,
): Promise<WorkspaceMigrationTableAction[]> {
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();

View File

@ -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,