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

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