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

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