feat: drop calendar repository (#5824)

This PR is replacing and removing all the raw queries and repositories
with the new `TwentyORM` and injection system using
`@InjectWorkspaceRepository`.
Some logic that was contained inside repositories has been moved to the
services.
In this PR we're only replacing repositories for calendar feature.

---------

Co-authored-by: Weiko <corentin@twenty.com>
Co-authored-by: bosiraphael <raphael.bosi@gmail.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Jérémy M
2024-06-22 09:26:58 +02:00
committed by GitHub
parent 91b0c2bb8e
commit 0b4bfce324
90 changed files with 979 additions and 1541 deletions

View File

@ -19,8 +19,8 @@ export class AnalyticsService {
async create(
createEventInput: CreateEventInput,
userId: string | undefined,
workspaceId: string | undefined,
userId: string | null | undefined,
workspaceId: string | null | undefined,
workspaceDisplayName: string | undefined,
workspaceDomainName: string | undefined,
hostName: string | undefined,

View File

@ -28,6 +28,8 @@ import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/s
import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-channel.workspace-entity';
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { AuthResolver } from './auth.resolver';
@ -60,11 +62,12 @@ const jwtModule = JwtModule.registerAsync({
ObjectMetadataRepositoryModule.forFeature([
ConnectedAccountWorkspaceEntity,
MessageChannelWorkspaceEntity,
CalendarChannelWorkspaceEntity,
]),
HttpModule,
UserWorkspaceModule,
OnboardingModule,
TwentyORMModule.forFeature([CalendarChannelWorkspaceEntity]),
WorkspaceDataSourceModule,
],
controllers: [
GoogleAuthController,

View File

@ -138,6 +138,7 @@ export class AuthResolver {
}
const transientToken = await this.tokenService.generateTransientToken(
workspaceMember.id,
user.id,
user.defaultWorkspace.id,
);

View File

@ -16,9 +16,7 @@ import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-
import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { LoadServiceWithWorkspaceContext } from 'src/engine/twenty-orm/context/load-service-with-workspace.context';
@Controller('auth/google-apis')
export class GoogleAPIsAuthController {
@ -27,8 +25,7 @@ export class GoogleAPIsAuthController {
private readonly tokenService: TokenService,
private readonly environmentService: EnvironmentService,
private readonly onboardingService: OnboardingService,
@InjectObjectMetadataRepository(WorkspaceMemberWorkspaceEntity)
private readonly workspaceMemberService: WorkspaceMemberRepository,
private readonly loadServiceWithWorkspaceContext: LoadServiceWithWorkspaceContext,
) {}
@Get()
@ -56,7 +53,7 @@ export class GoogleAPIsAuthController {
messageVisibility,
} = user;
const { workspaceMemberId, workspaceId } =
const { workspaceMemberId, userId, workspaceId } =
await this.tokenService.verifyTransientToken(transientToken);
const demoWorkspaceIds = this.environmentService.get('DEMO_WORKSPACE_IDS');
@ -71,7 +68,13 @@ export class GoogleAPIsAuthController {
throw new Error('Workspace not found');
}
await this.googleAPIsService.refreshGoogleRefreshToken({
const googleAPIsServiceInstance =
await this.loadServiceWithWorkspaceContext.load(
this.googleAPIsService,
workspaceId,
);
await googleAPIsServiceInstance.refreshGoogleRefreshToken({
handle: email,
workspaceMemberId: workspaceMemberId,
workspaceId: workspaceId,
@ -81,12 +84,14 @@ export class GoogleAPIsAuthController {
messageVisibility,
});
const userId = (
await this.workspaceMemberService.find(workspaceMemberId, workspaceId)
)?.userId;
if (userId) {
await this.onboardingService.skipSyncEmailOnboardingStep(
const onboardingServiceInstance =
await this.loadServiceWithWorkspaceContext.load(
this.onboardingService,
workspaceId,
);
await onboardingServiceInstance.skipSyncEmailOnboardingStep(
userId,
workspaceId,
);

View File

@ -3,17 +3,14 @@ import { Injectable } from '@nestjs/common';
import { EntityManager } from 'typeorm';
import { v4 } from 'uuid';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import {
GoogleCalendarSyncJobData,
GoogleCalendarSyncJob,
} from 'src/modules/calendar/jobs/google-calendar-sync.job';
import { CalendarChannelRepository } from 'src/modules/calendar/repositories/calendar-channel.repository';
import {
CalendarChannelWorkspaceEntity,
CalendarChannelVisibility,
@ -35,12 +32,16 @@ import {
MessagingMessageListFetchJobData,
} from 'src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job';
import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator';
import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
import { InjectWorkspaceDatasource } from 'src/engine/twenty-orm/decorators/inject-workspace-datasource.decorator';
@Injectable()
export class GoogleAPIsService {
constructor(
private readonly dataSourceService: DataSourceService,
private readonly typeORMService: TypeORMService,
@InjectWorkspaceDatasource()
private readonly workspaceDataSource: WorkspaceDataSource,
@InjectMessageQueue(MessageQueue.messagingQueue)
private readonly messageQueueService: MessageQueueService,
@InjectMessageQueue(MessageQueue.calendarQueue)
@ -50,8 +51,8 @@ export class GoogleAPIsService {
private readonly connectedAccountRepository: ConnectedAccountRepository,
@InjectObjectMetadataRepository(MessageChannelWorkspaceEntity)
private readonly messageChannelRepository: MessageChannelRepository,
@InjectObjectMetadataRepository(CalendarChannelWorkspaceEntity)
private readonly calendarChannelRepository: CalendarChannelRepository,
@InjectWorkspaceRepository(CalendarChannelWorkspaceEntity)
private readonly calendarChannelRepository: WorkspaceRepository<CalendarChannelWorkspaceEntity>,
) {}
async refreshGoogleRefreshToken(input: {
@ -71,14 +72,6 @@ export class GoogleAPIsService {
messageVisibility,
} = input;
const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
workspaceId,
);
const workspaceDataSource =
await this.typeORMService.connectToDataSource(dataSourceMetadata);
const isCalendarEnabled = this.environmentService.get(
'CALENDAR_PROVIDER_GOOGLE_ENABLED',
);
@ -93,65 +86,67 @@ export class GoogleAPIsService {
const existingAccountId = connectedAccounts?.[0]?.id;
const newOrExistingConnectedAccountId = existingAccountId ?? v4();
await workspaceDataSource?.transaction(async (manager: EntityManager) => {
if (!existingAccountId) {
await this.connectedAccountRepository.create(
{
id: newOrExistingConnectedAccountId,
handle,
provider: ConnectedAccountProvider.GOOGLE,
accessToken: input.accessToken,
refreshToken: input.refreshToken,
accountOwnerId: workspaceMemberId,
},
workspaceId,
manager,
);
await this.messageChannelRepository.create(
{
id: v4(),
connectedAccountId: newOrExistingConnectedAccountId,
type: MessageChannelType.EMAIL,
handle,
visibility:
messageVisibility || MessageChannelVisibility.SHARE_EVERYTHING,
syncStatus: MessageChannelSyncStatus.ONGOING,
},
workspaceId,
manager,
);
if (isCalendarEnabled) {
await this.calendarChannelRepository.create(
await this.workspaceDataSource.transaction(
async (manager: EntityManager) => {
if (!existingAccountId) {
await this.connectedAccountRepository.create(
{
id: v4(),
connectedAccountId: newOrExistingConnectedAccountId,
id: newOrExistingConnectedAccountId,
handle,
visibility:
calendarVisibility ||
CalendarChannelVisibility.SHARE_EVERYTHING,
provider: ConnectedAccountProvider.GOOGLE,
accessToken: input.accessToken,
refreshToken: input.refreshToken,
accountOwnerId: workspaceMemberId,
},
workspaceId,
manager,
);
}
} else {
await this.connectedAccountRepository.updateAccessTokenAndRefreshToken(
input.accessToken,
input.refreshToken,
newOrExistingConnectedAccountId,
workspaceId,
manager,
);
await this.messageChannelRepository.resetSync(
newOrExistingConnectedAccountId,
workspaceId,
manager,
);
}
});
await this.messageChannelRepository.create(
{
id: v4(),
connectedAccountId: newOrExistingConnectedAccountId,
type: MessageChannelType.EMAIL,
handle,
visibility:
messageVisibility || MessageChannelVisibility.SHARE_EVERYTHING,
syncStatus: MessageChannelSyncStatus.ONGOING,
},
workspaceId,
manager,
);
if (isCalendarEnabled) {
await this.calendarChannelRepository.save(
{
id: v4(),
connectedAccountId: newOrExistingConnectedAccountId,
handle,
visibility:
calendarVisibility ||
CalendarChannelVisibility.SHARE_EVERYTHING,
},
{},
manager,
);
}
} else {
await this.connectedAccountRepository.updateAccessTokenAndRefreshToken(
input.accessToken,
input.refreshToken,
newOrExistingConnectedAccountId,
workspaceId,
manager,
);
await this.messageChannelRepository.resetSync(
newOrExistingConnectedAccountId,
workspaceId,
manager,
);
}
},
);
if (this.environmentService.get('MESSAGING_PROVIDER_GMAIL_ENABLED')) {
const messageChannels =

View File

@ -147,6 +147,7 @@ export class TokenService {
async generateTransientToken(
workspaceMemberId: string,
userId: string,
workspaceId: string,
): Promise<AuthToken> {
const secret = this.environmentService.get('LOGIN_TOKEN_SECRET');
@ -158,6 +159,7 @@ export class TokenService {
const expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn));
const jwtPayload = {
sub: workspaceMemberId,
userId,
workspaceId,
};
@ -234,6 +236,7 @@ export class TokenService {
async verifyTransientToken(transientToken: string): Promise<{
workspaceMemberId: string;
userId: string;
workspaceId: string;
}> {
const transientTokenSecret =
@ -243,6 +246,7 @@ export class TokenService {
return {
workspaceMemberId: payload.sub,
userId: payload.userId,
workspaceId: payload.workspaceId,
};
}

View File

@ -203,9 +203,7 @@ export class BillingService {
: frontBaseUrl;
const quantity =
(await this.userWorkspaceService.getWorkspaceMemberCount(
user.defaultWorkspaceId,
)) || 1;
(await this.userWorkspaceService.getWorkspaceMemberCount()) || 1;
const stripeCustomerId = (
await this.billingSubscriptionRepository.findOneBy({

View File

@ -1,4 +1,4 @@
import { Logger } from '@nestjs/common';
import { Logger, Scope } from '@nestjs/common';
import { BillingService } from 'src/engine/core-modules/billing/billing.service';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
@ -8,7 +8,10 @@ import { MessageQueue } from 'src/engine/integrations/message-queue/message-queu
import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator';
export type UpdateSubscriptionJobData = { workspaceId: string };
@Processor(MessageQueue.billingQueue)
@Processor({
queueName: MessageQueue.billingQueue,
scope: Scope.REQUEST,
})
export class UpdateSubscriptionJob {
protected readonly logger = new Logger(UpdateSubscriptionJob.name);
@ -21,7 +24,7 @@ export class UpdateSubscriptionJob {
@Process(UpdateSubscriptionJob.name)
async handle(data: UpdateSubscriptionJobData): Promise<void> {
const workspaceMembersCount =
await this.userWorkspaceService.getWorkspaceMemberCount(data.workspaceId);
await this.userWorkspaceService.getWorkspaceMemberCount();
if (!workspaceMembersCount || workspaceMembersCount <= 0) {
return;

View File

@ -5,10 +5,10 @@ import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/
@ObjectType('TimelineCalendarEventParticipant')
export class TimelineCalendarEventParticipant {
@Field(() => UUIDScalarType, { nullable: true })
personId: string;
personId: string | null;
@Field(() => UUIDScalarType, { nullable: true })
workspaceMemberId: string;
workspaceMemberId: string | null;
@Field()
firstName: string;

View File

@ -81,19 +81,19 @@ export class TimelineCalendarEventService {
const participants = event.calendarEventParticipants.map(
(participant) => ({
calendarEventId: event.id,
personId: participant.person?.id,
workspaceMemberId: participant.workspaceMember?.id,
personId: participant.person?.id ?? null,
workspaceMemberId: participant.workspaceMember?.id ?? null,
firstName:
participant.person?.name.firstName ||
participant.person?.name?.firstName ||
participant.workspaceMember?.name.firstName ||
'',
lastName:
participant.person?.name.lastName ||
participant.person?.name?.lastName ||
participant.workspaceMember?.name.lastName ||
'',
displayName:
participant.person?.name.firstName ||
participant.person?.name.lastName ||
participant.person?.name?.firstName ||
participant.person?.name?.lastName ||
participant.workspaceMember?.name.firstName ||
participant.workspaceMember?.name.lastName ||
'',

View File

@ -56,7 +56,7 @@ export class OnboardingService {
const isInviteTeamSkipped =
inviteTeamValue === OnboardingStepValues.SKIPPED;
const workspaceMemberCount =
await this.userWorkspaceService.getWorkspaceMemberCount(workspace.id);
await this.userWorkspaceService.getWorkspaceMemberCount();
return (
!isInviteTeamSkipped &&

View File

@ -9,6 +9,8 @@ import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/use
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { User } from 'src/engine/core-modules/user/user.entity';
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Module({
imports: [
@ -21,6 +23,7 @@ import { User } from 'src/engine/core-modules/user/user.entity';
],
services: [UserWorkspaceService],
}),
TwentyORMModule.forFeature([WorkspaceMemberWorkspaceEntity]),
],
exports: [UserWorkspaceService],
providers: [UserWorkspaceService],

View File

@ -8,11 +8,12 @@ import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-works
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { assert } from 'src/utils/assert';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
constructor(
@ -20,9 +21,10 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
private readonly userWorkspaceRepository: Repository<UserWorkspace>,
@InjectRepository(User, 'core')
private readonly userRepository: Repository<User>,
@InjectWorkspaceRepository(WorkspaceMemberWorkspaceEntity)
private readonly workspaceMemberRepository: WorkspaceRepository<WorkspaceMemberWorkspaceEntity>,
private readonly dataSourceService: DataSourceService,
private readonly typeORMService: TypeORMService,
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
private eventEmitter: EventEmitter2,
) {
super(userWorkspaceRepository);
@ -99,23 +101,10 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
});
}
public async getWorkspaceMemberCount(
workspaceId: string,
): Promise<number | undefined> {
try {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
public async getWorkspaceMemberCount(): Promise<number | undefined> {
const workspaceMemberCount = await this.workspaceMemberRepository.count();
return (
await this.workspaceDataSourceService.executeRawQuery(
`SELECT * FROM ${dataSourceSchema}."workspaceMember"`,
[],
workspaceId,
)
).length;
} catch {
return undefined;
}
return workspaceMemberCount;
}
async checkUserWorkspaceExists(

View File

@ -29,6 +29,7 @@ import { User } from 'src/engine/core-modules/user/user.entity';
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
import { OnboardingStep } from 'src/engine/core-modules/onboarding/enums/onboarding-step.enum';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { LoadServiceWithWorkspaceContext } from 'src/engine/twenty-orm/context/load-service-with-workspace.context';
const getHMACKey = (email?: string, key?: string | null) => {
if (!email || !key) return null;
@ -48,6 +49,7 @@ export class UserResolver {
private readonly environmentService: EnvironmentService,
private readonly fileUploadService: FileUploadService,
private readonly onboardingService: OnboardingService,
private readonly loadServiceWithWorkspaceContext: LoadServiceWithWorkspaceContext,
) {}
@Query(() => User)
@ -122,9 +124,11 @@ export class UserResolver {
return null;
}
return this.onboardingService.getOnboardingStep(
user,
user.defaultWorkspace,
const contextInstance = await this.loadServiceWithWorkspaceContext.load(
this.onboardingService,
user.defaultWorkspaceId,
);
return contextInstance.getOnboardingStep(user, user.defaultWorkspace);
}
}

View File

@ -47,7 +47,11 @@ export class PgBossDriver
}
: {},
async (job) => {
await handler({ data: job.data, id: job.id, name: job.name });
await handler({
data: job.data,
id: job.id,
name: job.name.split('.')[1],
});
},
);
}

View File

@ -156,7 +156,7 @@ export class MessageQueueExplorer implements OnModuleInit {
}),
);
if (isRequestScoped) {
if (isRequestScoped && job.data) {
const contextId = createContextId();
if (this.moduleRef.registerRequestByContextId) {

View File

@ -1,15 +1,9 @@
import { CalendarChannelEventAssociationRepository } from 'src/modules/calendar/repositories/calendar-channel-event-association.repository';
import { CalendarChannelRepository } from 'src/modules/calendar/repositories/calendar-channel.repository';
import { CalendarEventParticipantRepository } from 'src/modules/calendar/repositories/calendar-event-participant.repository';
import { CalendarEventRepository } from 'src/modules/calendar/repositories/calendar-event.repository';
import { CompanyRepository } from 'src/modules/company/repositories/company.repository';
import { BlocklistRepository } from 'src/modules/connected-account/repositories/blocklist.repository';
import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository';
import { AuditLogRepository } from 'src/modules/timeline/repositiories/audit-log.repository';
import { TimelineActivityRepository } from 'src/modules/timeline/repositiories/timeline-activity.repository';
import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository';
import { AttachmentRepository } from 'src/modules/attachment/repositories/attachment.repository';
import { CommentRepository } from 'src/modules/activity/repositories/comment.repository';
import { MessageChannelMessageAssociationRepository } from 'src/modules/messaging/common/repositories/message-channel-message-association.repository';
import { MessageChannelRepository } from 'src/modules/messaging/common/repositories/message-channel.repository';
import { MessageParticipantRepository } from 'src/modules/messaging/common/repositories/message-participant.repository';
@ -20,11 +14,6 @@ import { PersonRepository } from 'src/modules/person/repositories/person.reposit
export const metadataToRepositoryMapping = {
AuditLogWorkspaceEntity: AuditLogRepository,
BlocklistWorkspaceEntity: BlocklistRepository,
CalendarChannelEventAssociationWorkspaceEntity:
CalendarChannelEventAssociationRepository,
CalendarChannelWorkspaceEntity: CalendarChannelRepository,
CalendarEventParticipantWorkspaceEntity: CalendarEventParticipantRepository,
CalendarEventWorkspaceEntity: CalendarEventRepository,
CompanyWorkspaceEntity: CompanyRepository,
ConnectedAccountWorkspaceEntity: ConnectedAccountRepository,
MessageChannelMessageAssociationWorkspaceEntity:
@ -36,6 +25,4 @@ export const metadataToRepositoryMapping = {
PersonWorkspaceEntity: PersonRepository,
TimelineActivityWorkspaceEntity: TimelineActivityRepository,
WorkspaceMemberWorkspaceEntity: WorkspaceMemberRepository,
AttachmentWorkspaceEntity: AttachmentRepository,
CommentWorkspaceEntity: CommentRepository,
};

View File

@ -0,0 +1,39 @@
import { Inject, Type } from '@nestjs/common';
import { ModuleRef, createContextId } from '@nestjs/core';
import { Injector } from '@nestjs/core/injector/injector';
export class LoadServiceWithWorkspaceContext {
private readonly injector = new Injector();
constructor(
@Inject(ModuleRef)
private readonly moduleRef: ModuleRef,
) {}
async load<T>(service: T, workspaceId: string): Promise<T> {
const modules = this.moduleRef['container'].getModules();
const host = [...modules.values()].find((module) =>
module.providers.has((service as Type<T>).constructor),
);
if (!host) {
throw new Error('Host module not found for the service');
}
const contextId = createContextId();
if (this.moduleRef.registerRequestByContextId) {
this.moduleRef.registerRequestByContextId(
{ req: { workspaceId } },
contextId,
);
}
return this.injector.loadPerContext(
service,
host,
new Map(host.providers),
contextId,
);
}
}

View File

@ -36,7 +36,7 @@ export class CustomWorkspaceEntity extends BaseWorkspaceEntity {
})
@WorkspaceIsNullable()
@WorkspaceIsSystem()
position: number;
position: number | null;
@WorkspaceRelation({
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.activityTargets,

View File

@ -6,8 +6,8 @@ import {
QueryRunner,
} from 'typeorm';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/entity.manager';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
export class WorkspaceDataSource extends DataSource {
readonly manager: WorkspaceEntityManager;

View File

@ -32,10 +32,9 @@ export class WorkspaceDatasourceFactory {
dataSourceMetadata.url ??
this.environmentService.get('PG_DATABASE_URL'),
type: 'postgres',
// logging: this.environmentService.get('DEBUG_MODE')
// ? ['query', 'error']
// : ['error'],
logging: 'all',
logging: this.environmentService.get('DEBUG_MODE')
? ['query', 'error']
: ['error'],
schema: dataSourceMetadata.schema,
entities,
ssl: this.environmentService.get('PG_SSL_ALLOW_SELF_SIGNED')

View File

@ -1,6 +1,7 @@
import {
DeepPartial,
DeleteResult,
EntityManager,
FindManyOptions,
FindOneOptions,
FindOptionsWhere,
@ -29,9 +30,13 @@ export class WorkspaceRepository<
/**
* FIND METHODS
*/
override async find(options?: FindManyOptions<Entity>): Promise<Entity[]> {
override async find(
options?: FindManyOptions<Entity>,
entityManager?: EntityManager,
): Promise<Entity[]> {
const manager = entityManager || this.manager;
const computedOptions = this.transformOptions(options);
const result = await super.find(computedOptions);
const result = await manager.find(this.target, computedOptions);
const formattedResult = this.formatResult(result);
return formattedResult;
@ -39,9 +44,11 @@ export class WorkspaceRepository<
override async findBy(
where: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
entityManager?: EntityManager,
): Promise<Entity[]> {
const manager = entityManager || this.manager;
const computedOptions = this.transformOptions({ where });
const result = await super.findBy(computedOptions.where);
const result = await manager.findBy(this.target, computedOptions.where);
const formattedResult = this.formatResult(result);
return formattedResult;
@ -49,9 +56,11 @@ export class WorkspaceRepository<
override async findAndCount(
options?: FindManyOptions<Entity>,
entityManager?: EntityManager,
): Promise<[Entity[], number]> {
const manager = entityManager || this.manager;
const computedOptions = this.transformOptions(options);
const result = await super.findAndCount(computedOptions);
const result = await manager.findAndCount(this.target, computedOptions);
const formattedResult = this.formatResult(result);
return formattedResult;
@ -59,9 +68,14 @@ export class WorkspaceRepository<
override async findAndCountBy(
where: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
entityManager?: EntityManager,
): Promise<[Entity[], number]> {
const manager = entityManager || this.manager;
const computedOptions = this.transformOptions({ where });
const result = await super.findAndCountBy(computedOptions.where);
const result = await manager.findAndCountBy(
this.target,
computedOptions.where,
);
const formattedResult = this.formatResult(result);
return formattedResult;
@ -69,9 +83,11 @@ export class WorkspaceRepository<
override async findOne(
options: FindOneOptions<Entity>,
entityManager?: EntityManager,
): Promise<Entity | null> {
const manager = entityManager || this.manager;
const computedOptions = this.transformOptions(options);
const result = await super.findOne(computedOptions);
const result = await manager.findOne(this.target, computedOptions);
const formattedResult = this.formatResult(result);
return formattedResult;
@ -79,9 +95,11 @@ export class WorkspaceRepository<
override async findOneBy(
where: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
entityManager?: EntityManager,
): Promise<Entity | null> {
const manager = entityManager || this.manager;
const computedOptions = this.transformOptions({ where });
const result = await super.findOneBy(computedOptions.where);
const result = await manager.findOneBy(this.target, computedOptions.where);
const formattedResult = this.formatResult(result);
return formattedResult;
@ -89,9 +107,11 @@ export class WorkspaceRepository<
override async findOneOrFail(
options: FindOneOptions<Entity>,
entityManager?: EntityManager,
): Promise<Entity> {
const manager = entityManager || this.manager;
const computedOptions = this.transformOptions(options);
const result = await super.findOneOrFail(computedOptions);
const result = await manager.findOneOrFail(this.target, computedOptions);
const formattedResult = this.formatResult(result);
return formattedResult;
@ -99,9 +119,14 @@ export class WorkspaceRepository<
override async findOneByOrFail(
where: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
entityManager?: EntityManager,
): Promise<Entity> {
const manager = entityManager || this.manager;
const computedOptions = this.transformOptions({ where });
const result = await super.findOneByOrFail(computedOptions.where);
const result = await manager.findOneByOrFail(
this.target,
computedOptions.where,
);
const formattedResult = this.formatResult(result);
return formattedResult;
@ -113,29 +138,40 @@ export class WorkspaceRepository<
override save<T extends DeepPartial<Entity>>(
entities: T[],
options: SaveOptions & { reload: false },
entityManager?: EntityManager,
): Promise<T[]>;
override save<T extends DeepPartial<Entity>>(
entities: T[],
options?: SaveOptions,
entityManager?: EntityManager,
): Promise<(T & Entity)[]>;
override save<T extends DeepPartial<Entity>>(
entity: T,
options: SaveOptions & { reload: false },
entityManager?: EntityManager,
): Promise<T>;
override save<T extends DeepPartial<Entity>>(
entity: T,
options?: SaveOptions,
entityManager?: EntityManager,
): Promise<T & Entity>;
override async save<T extends DeepPartial<Entity>>(
entityOrEntities: T | T[],
options?: SaveOptions,
entityManager?: EntityManager,
): Promise<T | T[]> {
const manager = entityManager || this.manager;
const formattedEntityOrEntities = this.formatData(entityOrEntities);
const result = await super.save(formattedEntityOrEntities as any, options);
const result = await manager.save(
this.target,
formattedEntityOrEntities as any,
options,
);
const formattedResult = this.formatResult(result);
return formattedResult;
@ -147,15 +183,27 @@ export class WorkspaceRepository<
override remove(
entities: Entity[],
options?: RemoveOptions,
entityManager?: EntityManager,
): Promise<Entity[]>;
override remove(entity: Entity, options?: RemoveOptions): Promise<Entity>;
override remove(
entity: Entity,
options?: RemoveOptions,
entityManager?: EntityManager,
): Promise<Entity>;
override async remove(
entityOrEntities: Entity | Entity[],
options?: RemoveOptions,
entityManager?: EntityManager,
): Promise<Entity | Entity[]> {
const manager = entityManager || this.manager;
const formattedEntityOrEntities = this.formatData(entityOrEntities);
const result = await super.remove(formattedEntityOrEntities as any);
const result = await manager.remove(
this.target,
formattedEntityOrEntities,
options,
);
const formattedResult = this.formatResult(result);
return formattedResult;
@ -172,40 +220,50 @@ export class WorkspaceRepository<
| ObjectId
| ObjectId[]
| FindOptionsWhere<Entity>,
entityManager?: EntityManager,
): Promise<DeleteResult> {
const manager = entityManager || this.manager;
if (typeof criteria === 'object' && 'where' in criteria) {
criteria = this.transformOptions(criteria);
}
return this.delete(criteria);
return manager.delete(this.target, criteria);
}
override softRemove<T extends DeepPartial<Entity>>(
entities: T[],
options: SaveOptions & { reload: false },
entityManager?: EntityManager,
): Promise<T[]>;
override softRemove<T extends DeepPartial<Entity>>(
entities: T[],
options?: SaveOptions,
entityManager?: EntityManager,
): Promise<(T & Entity)[]>;
override softRemove<T extends DeepPartial<Entity>>(
entity: T,
options: SaveOptions & { reload: false },
entityManager?: EntityManager,
): Promise<T>;
override softRemove<T extends DeepPartial<Entity>>(
entity: T,
options?: SaveOptions,
entityManager?: EntityManager,
): Promise<T & Entity>;
override async softRemove<T extends DeepPartial<Entity>>(
entityOrEntities: T | T[],
options?: SaveOptions,
entityManager?: EntityManager,
): Promise<T | T[]> {
const manager = entityManager || this.manager;
const formattedEntityOrEntities = this.formatData(entityOrEntities);
const result = await super.softRemove(
const result = await manager.softRemove(
this.target,
formattedEntityOrEntities as any,
options,
);
@ -225,12 +283,15 @@ export class WorkspaceRepository<
| ObjectId
| ObjectId[]
| FindOptionsWhere<Entity>,
entityManager?: EntityManager,
): Promise<UpdateResult> {
const manager = entityManager || this.manager;
if (typeof criteria === 'object' && 'where' in criteria) {
criteria = this.transformOptions(criteria);
}
return this.softDelete(criteria);
return manager.softDelete(this.target, criteria);
}
/**
@ -239,29 +300,36 @@ export class WorkspaceRepository<
override recover<T extends DeepPartial<Entity>>(
entities: T[],
options: SaveOptions & { reload: false },
entityManager?: EntityManager,
): Promise<T[]>;
override recover<T extends DeepPartial<Entity>>(
entities: T[],
options?: SaveOptions,
entityManager?: EntityManager,
): Promise<(T & Entity)[]>;
override recover<T extends DeepPartial<Entity>>(
entity: T,
options: SaveOptions & { reload: false },
entityManager?: EntityManager,
): Promise<T>;
override recover<T extends DeepPartial<Entity>>(
entity: T,
options?: SaveOptions,
entityManager?: EntityManager,
): Promise<T & Entity>;
override async recover<T extends DeepPartial<Entity>>(
entityOrEntities: T | T[],
options?: SaveOptions,
entityManager?: EntityManager,
): Promise<T | T[]> {
const manager = entityManager || this.manager;
const formattedEntityOrEntities = this.formatData(entityOrEntities);
const result = await super.recover(
const result = await manager.recover(
this.target,
formattedEntityOrEntities as any,
options,
);
@ -281,12 +349,15 @@ export class WorkspaceRepository<
| ObjectId
| ObjectId[]
| FindOptionsWhere<Entity>,
entityManager?: EntityManager,
): Promise<UpdateResult> {
const manager = entityManager || this.manager;
if (typeof criteria === 'object' && 'where' in criteria) {
criteria = this.transformOptions(criteria);
}
return this.restore(criteria);
return manager.restore(this.target, criteria);
}
/**
@ -294,9 +365,11 @@ export class WorkspaceRepository<
*/
override async insert(
entity: QueryDeepPartialEntity<Entity> | QueryDeepPartialEntity<Entity>[],
entityManager?: EntityManager,
): Promise<InsertResult> {
const manager = entityManager || this.manager;
const formatedEntity = this.formatData(entity);
const result = await super.insert(formatedEntity);
const result = await manager.insert(this.target, formatedEntity);
const formattedResult = this.formatResult(result);
return formattedResult;
@ -317,12 +390,15 @@ export class WorkspaceRepository<
| ObjectId[]
| FindOptionsWhere<Entity>,
partialEntity: QueryDeepPartialEntity<Entity>,
entityManager?: EntityManager,
): Promise<UpdateResult> {
const manager = entityManager || this.manager;
if (typeof criteria === 'object' && 'where' in criteria) {
criteria = this.transformOptions(criteria);
}
return this.update(criteria, partialEntity);
return manager.update(this.target, criteria, partialEntity);
}
override upsert(
@ -330,50 +406,63 @@ export class WorkspaceRepository<
| QueryDeepPartialEntity<Entity>
| QueryDeepPartialEntity<Entity>[],
conflictPathsOrOptions: string[] | UpsertOptions<Entity>,
entityManager?: EntityManager,
): Promise<InsertResult> {
const manager = entityManager || this.manager;
const formattedEntityOrEntities = this.formatData(entityOrEntities);
return this.upsert(formattedEntityOrEntities, conflictPathsOrOptions);
return manager.upsert(
this.target,
formattedEntityOrEntities,
conflictPathsOrOptions,
);
}
/**
* EXIST METHODS
*/
override exist(options?: FindManyOptions<Entity>): Promise<boolean> {
override exists(
options?: FindManyOptions<Entity>,
entityManager?: EntityManager,
): Promise<boolean> {
const manager = entityManager || this.manager;
const computedOptions = this.transformOptions(options);
return super.exist(computedOptions);
}
override exists(options?: FindManyOptions<Entity>): Promise<boolean> {
const computedOptions = this.transformOptions(options);
return super.exists(computedOptions);
return manager.exists(this.target, computedOptions);
}
override existsBy(
where: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
entityManager?: EntityManager,
): Promise<boolean> {
const manager = entityManager || this.manager;
const computedOptions = this.transformOptions({ where });
return super.existsBy(computedOptions.where);
return manager.existsBy(this.target, computedOptions.where);
}
/**
* COUNT METHODS
*/
override count(options?: FindManyOptions<Entity>): Promise<number> {
override count(
options?: FindManyOptions<Entity>,
entityManager?: EntityManager,
): Promise<number> {
const manager = entityManager || this.manager;
const computedOptions = this.transformOptions(options);
return super.count(computedOptions);
return manager.count(this.target, computedOptions);
}
override countBy(
where: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
entityManager?: EntityManager,
): Promise<number> {
const manager = entityManager || this.manager;
const computedOptions = this.transformOptions({ where });
return super.countBy(computedOptions.where);
return manager.countBy(this.target, computedOptions.where);
}
/**
@ -382,57 +471,79 @@ export class WorkspaceRepository<
override sum(
columnName: PickKeysByType<Entity, number>,
where?: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
entityManager?: EntityManager,
): Promise<number | null> {
const manager = entityManager || this.manager;
const computedOptions = this.transformOptions({ where });
return super.sum(columnName, computedOptions.where);
return manager.sum(this.target, columnName, computedOptions.where);
}
override average(
columnName: PickKeysByType<Entity, number>,
where?: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
entityManager?: EntityManager,
): Promise<number | null> {
const manager = entityManager || this.manager;
const computedOptions = this.transformOptions({ where });
return super.average(columnName, computedOptions.where);
return manager.average(this.target, columnName, computedOptions.where);
}
override minimum(
columnName: PickKeysByType<Entity, number>,
where?: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
entityManager?: EntityManager,
): Promise<number | null> {
const manager = entityManager || this.manager;
const computedOptions = this.transformOptions({ where });
return super.minimum(columnName, computedOptions.where);
return manager.minimum(this.target, columnName, computedOptions.where);
}
override maximum(
columnName: PickKeysByType<Entity, number>,
where?: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
entityManager?: EntityManager,
): Promise<number | null> {
const manager = entityManager || this.manager;
const computedOptions = this.transformOptions({ where });
return super.maximum(columnName, computedOptions.where);
return manager.maximum(this.target, columnName, computedOptions.where);
}
override increment(
conditions: FindOptionsWhere<Entity>,
propertyPath: string,
value: number | string,
entityManager?: EntityManager,
): Promise<UpdateResult> {
const manager = entityManager || this.manager;
const computedConditions = this.transformOptions({ where: conditions });
return this.increment(computedConditions.where, propertyPath, value);
return manager.increment(
this.target,
computedConditions.where,
propertyPath,
value,
);
}
override decrement(
conditions: FindOptionsWhere<Entity>,
propertyPath: string,
value: number | string,
entityManager?: EntityManager,
): Promise<UpdateResult> {
const manager = entityManager || this.manager;
const computedConditions = this.transformOptions({ where: conditions });
return this.decrement(computedConditions.where, propertyPath, value);
return manager.decrement(
this.target,
computedConditions.where,
propertyPath,
value,
);
}
/**

View File

@ -30,12 +30,21 @@ import {
ConfigurableModuleClass,
MODULE_OPTIONS_TOKEN,
} from 'src/engine/twenty-orm/twenty-orm.module-definition';
import { LoadServiceWithWorkspaceContext } from 'src/engine/twenty-orm/context/load-service-with-workspace.context';
@Global()
@Module({
imports: [DataSourceModule],
providers: [...entitySchemaFactories, TwentyORMManager],
exports: [EntitySchemaFactory, TwentyORMManager],
providers: [
...entitySchemaFactories,
TwentyORMManager,
LoadServiceWithWorkspaceContext,
],
exports: [
EntitySchemaFactory,
TwentyORMManager,
LoadServiceWithWorkspaceContext,
],
})
export class TwentyORMCoreModule
extends ConfigurableModuleClass

View File

@ -1,19 +1,20 @@
import { ObjectLiteral } from 'typeorm';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
export type ObjectRecord<T extends ObjectLiteral> = {
[K in keyof T as T[K] extends BaseWorkspaceEntity
? `${Extract<K, string>}Id`
: K]: T[K] extends BaseWorkspaceEntity
? string
: T[K] extends BaseWorkspaceEntity[]
? string[]
: T[K];
} & {
[K in keyof T]: T[K] extends BaseWorkspaceEntity
? ObjectRecord<T[K]>
: T[K] extends BaseWorkspaceEntity[]
? ObjectRecord<T[K][number]>[]
: T[K];
type RelationKeys<T> = {
[K in keyof T]: NonNullable<T[K]> extends BaseWorkspaceEntity ? K : never;
}[keyof T];
type ForeignKeyMap<T> = {
[K in RelationKeys<T> as `${K & string}Id`]: string;
};
type RecursiveObjectRecord<T> = {
[P in keyof T]: NonNullable<T[P]> extends BaseWorkspaceEntity
? ObjectRecord<NonNullable<T[P]>> & ForeignKeyMap<NonNullable<T[P]>>
: T[P];
};
// TODO: We should get rid of that it's causing too much issues
// Some relations can be null or undefined because they're not mendatory and other cannot
// This utility type put as defined all the joinColumn, so it's not well typed
export type ObjectRecord<T> = RecursiveObjectRecord<T> & ForeignKeyMap<T>;