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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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>;

View File

@ -1,21 +0,0 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
@Injectable()
export class CommentRepository {
constructor(
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
) {}
async deleteByAuthorId(authorId: string, workspaceId: string): Promise<void> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
await this.workspaceDataSourceService.executeRawQuery(
`DELETE FROM ${dataSourceSchema}."comment" WHERE "authorId" = $1`,
[authorId],
workspaceId,
);
}
}

View File

@ -36,7 +36,7 @@ export class ActivityTargetWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideFieldKey: 'activityTargets',
})
@WorkspaceIsNullable()
activity: Relation<ActivityWorkspaceEntity>;
activity: Relation<ActivityWorkspaceEntity> | null;
@WorkspaceRelation({
standardId: ACTIVITY_TARGET_STANDARD_FIELD_IDS.person,
@ -49,7 +49,7 @@ export class ActivityTargetWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideFieldKey: 'activityTargets',
})
@WorkspaceIsNullable()
person: Relation<PersonWorkspaceEntity>;
person: Relation<PersonWorkspaceEntity> | null;
@WorkspaceRelation({
standardId: ACTIVITY_TARGET_STANDARD_FIELD_IDS.company,
@ -62,7 +62,7 @@ export class ActivityTargetWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideFieldKey: 'activityTargets',
})
@WorkspaceIsNullable()
company: Relation<CompanyWorkspaceEntity>;
company: Relation<CompanyWorkspaceEntity> | null;
@WorkspaceRelation({
standardId: ACTIVITY_TARGET_STANDARD_FIELD_IDS.opportunity,
@ -75,7 +75,7 @@ export class ActivityTargetWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideFieldKey: 'activityTargets',
})
@WorkspaceIsNullable()
opportunity: Relation<OpportunityWorkspaceEntity>;
opportunity: Relation<OpportunityWorkspaceEntity> | null;
@WorkspaceDynamicRelation({
type: RelationMetadataType.MANY_TO_ONE,

View File

@ -64,7 +64,7 @@ export class ActivityWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconCalendarEvent',
})
@WorkspaceIsNullable()
reminderAt: Date;
reminderAt: Date | null;
@WorkspaceField({
standardId: ACTIVITY_STANDARD_FIELD_IDS.dueAt,
@ -74,7 +74,7 @@ export class ActivityWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconCalendarEvent',
})
@WorkspaceIsNullable()
dueAt: Date;
dueAt: Date | null;
@WorkspaceField({
standardId: ACTIVITY_STANDARD_FIELD_IDS.completedAt,
@ -84,7 +84,7 @@ export class ActivityWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconCheck',
})
@WorkspaceIsNullable()
completedAt: Date;
completedAt: Date | null;
@WorkspaceRelation({
standardId: ACTIVITY_STANDARD_FIELD_IDS.activityTargets,
@ -134,7 +134,7 @@ export class ActivityWorkspaceEntity extends BaseWorkspaceEntity {
joinColumn: 'authorId',
})
@WorkspaceIsNullable()
author: Relation<WorkspaceMemberWorkspaceEntity>;
author: Relation<WorkspaceMemberWorkspaceEntity> | null;
@WorkspaceRelation({
standardId: ACTIVITY_STANDARD_FIELD_IDS.assignee,
@ -148,5 +148,5 @@ export class ActivityWorkspaceEntity extends BaseWorkspaceEntity {
joinColumn: 'assigneeId',
})
@WorkspaceIsNullable()
assignee: Relation<WorkspaceMemberWorkspaceEntity>;
assignee: Relation<WorkspaceMemberWorkspaceEntity> | null;
}

View File

@ -45,5 +45,5 @@ export class ApiKeyWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconCalendar',
})
@WorkspaceIsNullable()
revokedAt?: Date;
revokedAt?: Date | null;
}

View File

@ -1,21 +0,0 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
@Injectable()
export class AttachmentRepository {
constructor(
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
) {}
async deleteByAuthorId(authorId: string, workspaceId: string): Promise<void> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
await this.workspaceDataSourceService.executeRawQuery(
`DELETE FROM ${dataSourceSchema}."attachment" WHERE "authorId" = $1`,
[authorId],
workspaceId,
);
}
}

View File

@ -80,7 +80,7 @@ export class AttachmentWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideFieldKey: 'attachments',
})
@WorkspaceIsNullable()
activity: Relation<ActivityWorkspaceEntity>;
activity: Relation<ActivityWorkspaceEntity> | null;
@WorkspaceRelation({
standardId: ATTACHMENT_STANDARD_FIELD_IDS.person,
@ -93,7 +93,7 @@ export class AttachmentWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideFieldKey: 'attachments',
})
@WorkspaceIsNullable()
person: Relation<PersonWorkspaceEntity>;
person: Relation<PersonWorkspaceEntity> | null;
@WorkspaceRelation({
standardId: ATTACHMENT_STANDARD_FIELD_IDS.company,
@ -106,7 +106,7 @@ export class AttachmentWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideFieldKey: 'attachments',
})
@WorkspaceIsNullable()
company: Relation<CompanyWorkspaceEntity>;
company: Relation<CompanyWorkspaceEntity> | null;
@WorkspaceRelation({
standardId: ATTACHMENT_STANDARD_FIELD_IDS.opportunity,
@ -119,7 +119,7 @@ export class AttachmentWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideFieldKey: 'attachments',
})
@WorkspaceIsNullable()
opportunity: Relation<OpportunityWorkspaceEntity>;
opportunity: Relation<OpportunityWorkspaceEntity> | null;
@WorkspaceDynamicRelation({
type: RelationMetadataType.MANY_TO_ONE,

View File

@ -1,3 +1,5 @@
import { Scope } from '@nestjs/common';
import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
@ -11,7 +13,10 @@ export type MatchParticipantJobData = {
workspaceMemberId?: string;
};
@Processor(MessageQueue.messagingQueue)
@Processor({
queueName: MessageQueue.messagingQueue,
scope: Scope.REQUEST,
})
export class MatchParticipantJob {
constructor(
private readonly messageParticipantService: MessagingMessageParticipantService,

View File

@ -1,3 +1,5 @@
import { Scope } from '@nestjs/common';
import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
import { CalendarEventParticipantService } from 'src/modules/calendar/services/calendar-event-participant/calendar-event-participant.service';
@ -11,7 +13,10 @@ export type UnmatchParticipantJobData = {
workspaceMemberId?: string;
};
@Processor(MessageQueue.messagingQueue)
@Processor({
queueName: MessageQueue.messagingQueue,
scope: Scope.REQUEST,
})
export class UnmatchParticipantJob {
constructor(
private readonly messageParticipantService: MessagingMessageParticipantService,

View File

@ -1,19 +1,10 @@
import { Module } from '@nestjs/common';
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
import { GoogleCalendarSyncCommand } from 'src/modules/calendar/commands/google-calendar-sync.command';
import { WorkspaceGoogleCalendarSyncModule } from 'src/modules/calendar/services/workspace-google-calendar-sync/workspace-google-calendar-sync.module';
import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-channel.workspace-entity';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
@Module({
imports: [
ObjectMetadataRepositoryModule.forFeature([
ConnectedAccountWorkspaceEntity,
CalendarChannelWorkspaceEntity,
]),
WorkspaceGoogleCalendarSyncModule,
],
imports: [WorkspaceGoogleCalendarSyncModule],
providers: [GoogleCalendarSyncCommand],
})
export class CalendarCommandsModule {}

View File

@ -1,4 +1,5 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Scope } from '@nestjs/common';
import { Repository, In } from 'typeorm';
@ -10,7 +11,10 @@ import { MessageQueue } from 'src/engine/integrations/message-queue/message-queu
import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator';
import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator';
@Processor(MessageQueue.cronQueue)
@Processor({
queueName: MessageQueue.cronQueue,
scope: Scope.REQUEST,
})
export class GoogleCalendarSyncCronJob {
constructor(
@InjectRepository(Workspace, 'core')

View File

@ -1,35 +1,38 @@
import { Logger } from '@nestjs/common';
import { Logger, Scope } from '@nestjs/common';
import { Any, ILike } from 'typeorm';
import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { CalendarChannelEventAssociationRepository } from 'src/modules/calendar/repositories/calendar-channel-event-association.repository';
import { CalendarChannelRepository } from 'src/modules/calendar/repositories/calendar-channel.repository';
import { CalendarEventCleanerService } from 'src/modules/calendar/services/calendar-event-cleaner/calendar-event-cleaner.service';
import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-channel-event-association.workspace-entity';
import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-channel.workspace-entity';
import { BlocklistRepository } from 'src/modules/connected-account/repositories/blocklist.repository';
import { BlocklistWorkspaceEntity } from 'src/modules/connected-account/standard-objects/blocklist.workspace-entity';
import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator';
import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
export type BlocklistItemDeleteCalendarEventsJobData = {
workspaceId: string;
blocklistItemId: string;
};
@Processor(MessageQueue.calendarQueue)
@Processor({
queueName: MessageQueue.calendarQueue,
scope: Scope.REQUEST,
})
export class BlocklistItemDeleteCalendarEventsJob {
private readonly logger = new Logger(
BlocklistItemDeleteCalendarEventsJob.name,
);
constructor(
@InjectObjectMetadataRepository(CalendarChannelWorkspaceEntity)
private readonly calendarChannelRepository: CalendarChannelRepository,
@InjectObjectMetadataRepository(
CalendarChannelEventAssociationWorkspaceEntity,
)
private readonly calendarChannelEventAssociationRepository: CalendarChannelEventAssociationRepository,
@InjectWorkspaceRepository(CalendarChannelWorkspaceEntity)
private readonly calendarChannelRepository: WorkspaceRepository<CalendarChannelWorkspaceEntity>,
@InjectWorkspaceRepository(CalendarChannelEventAssociationWorkspaceEntity)
private readonly calendarChannelEventAssociationRepository: WorkspaceRepository<CalendarChannelEventAssociationWorkspaceEntity>,
@InjectObjectMetadataRepository(BlocklistWorkspaceEntity)
private readonly blocklistRepository: BlocklistRepository,
private readonly calendarEventCleanerService: CalendarEventCleanerService,
@ -58,19 +61,39 @@ export class BlocklistItemDeleteCalendarEventsJob {
`Deleting calendar events from ${handle} in workspace ${workspaceId} for workspace member ${workspaceMemberId}`,
);
const calendarChannels =
await this.calendarChannelRepository.getIdsByWorkspaceMemberId(
workspaceMemberId,
workspaceId,
if (!workspaceMemberId) {
throw new Error(
`Workspace member ID is undefined for blocklist item ${blocklistItemId} in workspace ${workspaceId}`,
);
}
const calendarChannels = await this.calendarChannelRepository.find({
where: {
connectedAccount: {
accountOwner: {
id: workspaceMemberId,
},
},
},
relations: ['connectedAccount.accountOwner'],
});
const calendarChannelIds = calendarChannels.map(({ id }) => id);
await this.calendarChannelEventAssociationRepository.deleteByCalendarEventParticipantHandleAndCalendarChannelIds(
handle,
calendarChannelIds,
workspaceId,
);
const isHandleDomain = handle.startsWith('@');
await this.calendarChannelEventAssociationRepository.delete({
calendarEvent: {
calendarEventParticipants: {
handle: isHandleDomain ? ILike(`%${handle}`) : handle,
},
calendarChannelEventAssociations: {
calendarChannel: {
id: Any(calendarChannelIds),
},
},
},
});
await this.calendarEventCleanerService.cleanWorkspaceCalendarEvents(
workspaceId,

View File

@ -1,4 +1,4 @@
import { Logger } from '@nestjs/common';
import { Logger, Scope } from '@nestjs/common';
import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
@ -14,7 +14,10 @@ export type BlocklistReimportCalendarEventsJobData = {
handle: string;
};
@Processor(MessageQueue.calendarQueue)
@Processor({
queueName: MessageQueue.calendarQueue,
scope: Scope.REQUEST,
})
export class BlocklistReimportCalendarEventsJob {
private readonly logger = new Logger(BlocklistReimportCalendarEventsJob.name);

View File

@ -1,35 +1,35 @@
import { Logger } from '@nestjs/common';
import { Logger, Scope } from '@nestjs/common';
import { IsNull } from 'typeorm';
import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { CalendarChannelRepository } from 'src/modules/calendar/repositories/calendar-channel.repository';
import { CalendarEventParticipantRepository } from 'src/modules/calendar/repositories/calendar-event-participant.repository';
import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-channel.workspace-entity';
import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-event-participant.workspace-entity';
import { CreateCompanyAndContactService } from 'src/modules/connected-account/auto-companies-and-contacts-creation/services/create-company-and-contact.service';
import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator';
import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
export type CalendarCreateCompanyAndContactAfterSyncJobData = {
workspaceId: string;
calendarChannelId: string;
};
@Processor(MessageQueue.calendarQueue)
@Processor({
queueName: MessageQueue.calendarQueue,
scope: Scope.REQUEST,
})
export class CalendarCreateCompanyAndContactAfterSyncJob {
private readonly logger = new Logger(
CalendarCreateCompanyAndContactAfterSyncJob.name,
);
constructor(
private readonly createCompanyAndContactService: CreateCompanyAndContactService,
@InjectObjectMetadataRepository(CalendarChannelWorkspaceEntity)
private readonly calendarChannelService: CalendarChannelRepository,
@InjectObjectMetadataRepository(CalendarEventParticipantWorkspaceEntity)
private readonly calendarEventParticipantRepository: CalendarEventParticipantRepository,
@InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity)
private readonly connectedAccountRepository: ConnectedAccountRepository,
@InjectWorkspaceRepository(CalendarChannelWorkspaceEntity)
private readonly calendarChannelRepository: WorkspaceRepository<CalendarChannelWorkspaceEntity>,
@InjectWorkspaceRepository(CalendarEventParticipantWorkspaceEntity)
private readonly calendarEventParticipantRepository: WorkspaceRepository<CalendarEventParticipantWorkspaceEntity>,
) {}
@Process(CalendarCreateCompanyAndContactAfterSyncJob.name)
@ -41,40 +41,52 @@ export class CalendarCreateCompanyAndContactAfterSyncJob {
);
const { workspaceId, calendarChannelId } = data;
const calendarChannels = await this.calendarChannelService.getByIds(
[calendarChannelId],
workspaceId,
);
const calendarChannel = await this.calendarChannelRepository.findOne({
where: {
id: calendarChannelId,
},
relations: ['connectedAccount.accountOwner'],
});
if (calendarChannels.length === 0) {
if (!calendarChannel) {
throw new Error(
`Calendar channel with id ${calendarChannelId} not found in workspace ${workspaceId}`,
);
}
const { handle, isContactAutoCreationEnabled, connectedAccountId } =
calendarChannels[0];
const { handle, isContactAutoCreationEnabled, connectedAccount } =
calendarChannel;
if (!isContactAutoCreationEnabled || !handle) {
return;
}
const connectedAccount = await this.connectedAccountRepository.getById(
connectedAccountId,
workspaceId,
);
if (!connectedAccount) {
throw new Error(
`Connected account with id ${connectedAccountId} not found in workspace ${workspaceId}`,
`Connected account not found in workspace ${workspaceId}`,
);
}
const calendarEventParticipantsWithoutPersonIdAndWorkspaceMemberId =
await this.calendarEventParticipantRepository.getByCalendarChannelIdWithoutPersonIdAndWorkspaceMemberId(
calendarChannelId,
workspaceId,
);
await this.calendarEventParticipantRepository.find({
where: {
calendarEvent: {
calendarChannelEventAssociations: {
calendarChannel: {
id: calendarChannelId,
},
},
calendarEventParticipants: {
person: IsNull(),
workspaceMember: IsNull(),
},
},
},
relations: [
'calendarEvent.calendarChannelEventAssociations',
'calendarEvent.calendarEventParticipants',
],
});
await this.createCompanyAndContactService.createCompaniesAndContactsAndUpdateParticipants(
connectedAccount,

View File

@ -1,6 +1,7 @@
import { Module } from '@nestjs/common';
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
import { BlocklistItemDeleteCalendarEventsJob } from 'src/modules/calendar/jobs/blocklist-item-delete-calendar-events.job';
import { BlocklistReimportCalendarEventsJob } from 'src/modules/calendar/jobs/blocklist-reimport-calendar-events.job';
import { CalendarCreateCompanyAndContactAfterSyncJob } from 'src/modules/calendar/jobs/calendar-create-company-and-contact-after-sync.job';
@ -18,10 +19,12 @@ import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/s
@Module({
imports: [
ObjectMetadataRepositoryModule.forFeature([
TwentyORMModule.forFeature([
CalendarChannelWorkspaceEntity,
CalendarChannelEventAssociationWorkspaceEntity,
CalendarEventParticipantWorkspaceEntity,
]),
ObjectMetadataRepositoryModule.forFeature([
ConnectedAccountWorkspaceEntity,
BlocklistWorkspaceEntity,
]),

View File

@ -1,4 +1,4 @@
import { Logger } from '@nestjs/common';
import { Logger, Scope } from '@nestjs/common';
import { GoogleAPIRefreshAccessTokenService } from 'src/modules/connected-account/services/google-api-refresh-access-token/google-api-refresh-access-token.service';
import { GoogleCalendarSyncService } from 'src/modules/calendar/services/google-calendar-sync/google-calendar-sync.service';
@ -11,7 +11,10 @@ export type GoogleCalendarSyncJobData = {
connectedAccountId: string;
};
@Processor(MessageQueue.calendarQueue)
@Processor({
queueName: MessageQueue.calendarQueue,
scope: Scope.REQUEST,
})
export class GoogleCalendarSyncJob {
private readonly logger = new Logger(GoogleCalendarSyncJob.name);

View File

@ -25,7 +25,7 @@ export class CalendarEventParticipantListener {
@OnEvent('calendarEventParticipant.matched')
public async handleCalendarEventParticipantMatchedEvent(payload: {
workspaceId: string;
userId: string;
workspaceMemberId: string;
calendarEventParticipants: ObjectRecord<CalendarEventParticipantWorkspaceEntity>[];
}): Promise<void> {
const calendarEventParticipants = payload.calendarEventParticipants ?? [];
@ -59,7 +59,7 @@ export class CalendarEventParticipantListener {
properties: null,
objectName: 'calendarEvent',
recordId: participant.personId,
workspaceMemberId: payload.userId,
workspaceMemberId: payload.workspaceMemberId,
workspaceId: payload.workspaceId,
linkedObjectMetadataId: calendarEventObjectMetadata.id,
linkedRecordId: participant.calendarEventId,

View File

@ -1,26 +1,20 @@
import {
BadRequestException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { BadRequestException, Injectable } from '@nestjs/common';
import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface';
import { FindManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-channel-event-association.workspace-entity';
import { CalendarChannelEventAssociationRepository } from 'src/modules/calendar/repositories/calendar-channel-event-association.repository';
import { CanAccessCalendarEventService } from 'src/modules/calendar/query-hooks/calendar-event/services/can-access-calendar-event.service';
import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
@Injectable()
export class CalendarEventFindManyPreQueryHook
implements WorkspacePreQueryHook
{
constructor(
@InjectObjectMetadataRepository(
CalendarChannelEventAssociationWorkspaceEntity,
)
private readonly calendarChannelEventAssociationRepository: CalendarChannelEventAssociationRepository,
@InjectWorkspaceRepository(CalendarChannelEventAssociationWorkspaceEntity)
private readonly calendarChannelEventAssociationRepository: WorkspaceRepository<CalendarChannelEventAssociationWorkspaceEntity>,
private readonly canAccessCalendarEventService: CanAccessCalendarEventService,
) {}
@ -33,20 +27,25 @@ export class CalendarEventFindManyPreQueryHook
throw new BadRequestException('id filter is required');
}
const calendarChannelCalendarEventAssociations =
await this.calendarChannelEventAssociationRepository.getByCalendarEventIds(
[payload?.filter?.id?.eq],
workspaceId,
);
// TODO: Re-implement this using twenty ORM
// const calendarChannelCalendarEventAssociations =
// await this.calendarChannelEventAssociationRepository.find({
// where: {
// calendarEvent: {
// id: payload?.filter?.id?.eq,
// },
// },
// relations: ['calendarChannel.connectedAccount'],
// });
if (calendarChannelCalendarEventAssociations.length === 0) {
throw new NotFoundException();
}
// if (calendarChannelCalendarEventAssociations.length === 0) {
// throw new NotFoundException();
// }
await this.canAccessCalendarEventService.canAccessCalendarEvent(
userId,
workspaceId,
calendarChannelCalendarEventAssociations,
);
// await this.canAccessCalendarEventService.canAccessCalendarEvent(
// userId,
// workspaceId,
// calendarChannelCalendarEventAssociations,
// );
}
}

View File

@ -1,24 +1,18 @@
import {
BadRequestException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { BadRequestException, Injectable } from '@nestjs/common';
import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface';
import { FindOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { CanAccessCalendarEventService } from 'src/modules/calendar/query-hooks/calendar-event/services/can-access-calendar-event.service';
import { CalendarChannelEventAssociationRepository } from 'src/modules/calendar/repositories/calendar-channel-event-association.repository';
import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-channel-event-association.workspace-entity';
@Injectable()
export class CalendarEventFindOnePreQueryHook implements WorkspacePreQueryHook {
constructor(
@InjectObjectMetadataRepository(
CalendarChannelEventAssociationWorkspaceEntity,
)
private readonly calendarChannelEventAssociationRepository: CalendarChannelEventAssociationRepository,
@InjectWorkspaceRepository(CalendarChannelEventAssociationWorkspaceEntity)
private readonly calendarChannelEventAssociationRepository: WorkspaceRepository<CalendarChannelEventAssociationWorkspaceEntity>,
private readonly canAccessCalendarEventService: CanAccessCalendarEventService,
) {}
@ -31,20 +25,24 @@ export class CalendarEventFindOnePreQueryHook implements WorkspacePreQueryHook {
throw new BadRequestException('id filter is required');
}
const calendarChannelCalendarEventAssociations =
await this.calendarChannelEventAssociationRepository.getByCalendarEventIds(
[payload?.filter?.id?.eq],
workspaceId,
);
// TODO: Re-implement this using twenty ORM
// const calendarChannelCalendarEventAssociations =
// await this.calendarChannelEventAssociationRepository.find({
// where: {
// calendarEvent: {
// id: payload?.filter?.id?.eq,
// },
// },
// });
if (calendarChannelCalendarEventAssociations.length === 0) {
throw new NotFoundException();
}
// if (calendarChannelCalendarEventAssociations.length === 0) {
// throw new NotFoundException();
// }
await this.canAccessCalendarEventService.canAccessCalendarEvent(
userId,
workspaceId,
calendarChannelCalendarEventAssociations,
);
// await this.canAccessCalendarEventService.canAccessCalendarEvent(
// userId,
// workspaceId,
// calendarChannelCalendarEventAssociations,
// );
}
}

View File

@ -1,10 +1,11 @@
import { ForbiddenException, Injectable } from '@nestjs/common';
import groupBy from 'lodash.groupby';
import { Any } from 'typeorm';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record';
import { CalendarChannelRepository } from 'src/modules/calendar/repositories/calendar-channel.repository';
import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-channel-event-association.workspace-entity';
import {
CalendarChannelWorkspaceEntity,
@ -18,8 +19,8 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
@Injectable()
export class CanAccessCalendarEventService {
constructor(
@InjectObjectMetadataRepository(CalendarChannelWorkspaceEntity)
private readonly calendarChannelRepository: CalendarChannelRepository,
@InjectWorkspaceRepository(CalendarChannelWorkspaceEntity)
private readonly calendarChannelRepository: WorkspaceRepository<CalendarChannelWorkspaceEntity>,
@InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity)
private readonly connectedAccountRepository: ConnectedAccountRepository,
@InjectObjectMetadataRepository(WorkspaceMemberWorkspaceEntity)
@ -29,14 +30,17 @@ export class CanAccessCalendarEventService {
public async canAccessCalendarEvent(
userId: string,
workspaceId: string,
calendarChannelCalendarEventAssociations: ObjectRecord<CalendarChannelEventAssociationWorkspaceEntity>[],
calendarChannelCalendarEventAssociations: CalendarChannelEventAssociationWorkspaceEntity[],
) {
const calendarChannels = await this.calendarChannelRepository.getByIds(
calendarChannelCalendarEventAssociations.map(
(association) => association.calendarChannelId,
),
workspaceId,
);
const calendarChannels = await this.calendarChannelRepository.find({
where: {
id: Any(
calendarChannelCalendarEventAssociations.map(
(association) => association.calendarChannel.id,
),
),
},
});
const calendarChannelsGroupByVisibility = groupBy(
calendarChannels,
@ -56,7 +60,7 @@ export class CanAccessCalendarEventService {
const calendarChannelsConnectedAccounts =
await this.connectedAccountRepository.getByIds(
calendarChannels.map((channel) => channel.connectedAccountId),
calendarChannels.map((channel) => channel.connectedAccount.id),
workspaceId,
);

View File

@ -8,12 +8,15 @@ import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/standard-ob
import { CalendarEventFindManyPreQueryHook } from 'src/modules/calendar/query-hooks/calendar-event/calendar-event-find-many.pre-query.hook';
import { CalendarEventFindOnePreQueryHook } from 'src/modules/calendar/query-hooks/calendar-event/calendar-event-find-one.pre-query-hook';
import { CanAccessCalendarEventService } from 'src/modules/calendar/query-hooks/calendar-event/services/can-access-calendar-event.service';
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
@Module({
imports: [
ObjectMetadataRepositoryModule.forFeature([
TwentyORMModule.forFeature([
CalendarChannelEventAssociationWorkspaceEntity,
CalendarChannelWorkspaceEntity,
]),
ObjectMetadataRepositoryModule.forFeature([
ConnectedAccountWorkspaceEntity,
WorkspaceMemberWorkspaceEntity,
]),

View File

@ -1,205 +0,0 @@
import { Injectable } from '@nestjs/common';
import { EntityManager } from 'typeorm';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record';
import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-channel-event-association.workspace-entity';
import { getFlattenedValuesAndValuesStringForBatchRawQuery } from 'src/modules/calendar/utils/get-flattened-values-and-values-string-for-batch-raw-query.util';
@Injectable()
export class CalendarChannelEventAssociationRepository {
constructor(
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
) {}
public async getByEventExternalIdsAndCalendarChannelId(
eventExternalIds: string[],
calendarChannelId: string,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<ObjectRecord<CalendarChannelEventAssociationWorkspaceEntity>[]> {
if (eventExternalIds.length === 0) {
return [];
}
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
return await this.workspaceDataSourceService.executeRawQuery(
`SELECT * FROM ${dataSourceSchema}."calendarChannelEventAssociation"
WHERE "eventExternalId" = ANY($1) AND "calendarChannelId" = $2`,
[eventExternalIds, calendarChannelId],
workspaceId,
transactionManager,
);
}
public async deleteByEventExternalIdsAndCalendarChannelId(
eventExternalIds: string[],
calendarChannelId: string,
workspaceId: string,
transactionManager?: EntityManager,
) {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
await this.workspaceDataSourceService.executeRawQuery(
`DELETE FROM ${dataSourceSchema}."calendarChannelEventAssociation" WHERE "eventExternalId" = ANY($1) AND "calendarChannelId" = $2`,
[eventExternalIds, calendarChannelId],
workspaceId,
transactionManager,
);
}
public async getByCalendarChannelIds(
calendarChannelIds: string[],
workspaceId: string,
transactionManager?: EntityManager,
): Promise<ObjectRecord<CalendarChannelEventAssociationWorkspaceEntity>[]> {
if (calendarChannelIds.length === 0) {
return [];
}
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
return await this.workspaceDataSourceService.executeRawQuery(
`SELECT * FROM ${dataSourceSchema}."calendarChannelEventAssociation"
WHERE "calendarChannelId" = ANY($1)`,
[calendarChannelIds],
workspaceId,
transactionManager,
);
}
public async deleteByCalendarChannelIds(
calendarChannelIds: string[],
workspaceId: string,
transactionManager?: EntityManager,
) {
if (calendarChannelIds.length === 0) {
return;
}
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
await this.workspaceDataSourceService.executeRawQuery(
`DELETE FROM ${dataSourceSchema}."calendarChannelEventAssociation" WHERE "calendarChannelId" = ANY($1)`,
[calendarChannelIds],
workspaceId,
transactionManager,
);
}
public async deleteByIds(
ids: string[],
workspaceId: string,
transactionManager?: EntityManager,
) {
if (ids.length === 0) {
return;
}
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
await this.workspaceDataSourceService.executeRawQuery(
`DELETE FROM ${dataSourceSchema}."calendarChannelEventAssociation" WHERE "id" = ANY($1)`,
[ids],
workspaceId,
transactionManager,
);
}
public async getByCalendarEventIds(
calendarEventIds: string[],
workspaceId: string,
transactionManager?: EntityManager,
): Promise<ObjectRecord<CalendarChannelEventAssociationWorkspaceEntity>[]> {
if (calendarEventIds.length === 0) {
return [];
}
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
return await this.workspaceDataSourceService.executeRawQuery(
`SELECT * FROM ${dataSourceSchema}."calendarChannelEventAssociation"
WHERE "calendarEventId" = ANY($1)`,
[calendarEventIds],
workspaceId,
transactionManager,
);
}
public async saveCalendarChannelEventAssociations(
calendarChannelEventAssociations: Omit<
ObjectRecord<CalendarChannelEventAssociationWorkspaceEntity>,
'id' | 'createdAt' | 'updatedAt' | 'calendarChannel' | 'calendarEvent'
>[],
workspaceId: string,
transactionManager?: EntityManager,
) {
if (calendarChannelEventAssociations.length === 0) {
return;
}
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
const {
flattenedValues: calendarChannelEventAssociationValues,
valuesString,
} = getFlattenedValuesAndValuesStringForBatchRawQuery(
calendarChannelEventAssociations,
{
calendarChannelId: 'uuid',
calendarEventId: 'uuid',
eventExternalId: 'text',
},
);
await this.workspaceDataSourceService.executeRawQuery(
`INSERT INTO ${dataSourceSchema}."calendarChannelEventAssociation" ("calendarChannelId", "calendarEventId", "eventExternalId")
VALUES ${valuesString}`,
calendarChannelEventAssociationValues,
workspaceId,
transactionManager,
);
}
public async deleteByCalendarEventParticipantHandleAndCalendarChannelIds(
calendarEventParticipantHandle: string,
calendarChannelIds: string[],
workspaceId: string,
transactionManager?: EntityManager,
) {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
const isHandleDomain = calendarEventParticipantHandle.startsWith('@');
await this.workspaceDataSourceService.executeRawQuery(
`DELETE FROM ${dataSourceSchema}."calendarChannelEventAssociation"
WHERE "id" IN (
SELECT "calendarChannelEventAssociation"."id"
FROM ${dataSourceSchema}."calendarChannelEventAssociation" "calendarChannelEventAssociation"
JOIN ${dataSourceSchema}."calendarEvent" "calendarEvent" ON "calendarChannelEventAssociation"."calendarEventId" = "calendarEvent"."id"
JOIN ${dataSourceSchema}."calendarEventParticipant" "calendarEventParticipant" ON "calendarEvent"."id" = "calendarEventParticipant"."calendarEventId"
WHERE "calendarEventParticipant"."handle" ${
isHandleDomain ? 'ILIKE' : '='
} $1 AND "calendarChannelEventAssociation"."calendarChannelId" = ANY($2)
)`,
[
isHandleDomain
? `%${calendarEventParticipantHandle}`
: calendarEventParticipantHandle,
calendarChannelIds,
],
workspaceId,
transactionManager,
);
}
}

View File

@ -1,135 +0,0 @@
import { Injectable } from '@nestjs/common';
import { EntityManager } from 'typeorm';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-channel.workspace-entity';
import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record';
@Injectable()
export class CalendarChannelRepository {
constructor(
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
) {}
public async getAll(
workspaceId: string,
transactionManager?: EntityManager,
): Promise<ObjectRecord<CalendarChannelWorkspaceEntity>[]> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
return await this.workspaceDataSourceService.executeRawQuery(
`SELECT * FROM ${dataSourceSchema}."calendarChannel"`,
[],
workspaceId,
transactionManager,
);
}
public async create(
calendarChannel: Pick<
ObjectRecord<CalendarChannelWorkspaceEntity>,
'id' | 'connectedAccountId' | 'handle' | 'visibility'
>,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<void> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
await this.workspaceDataSourceService.executeRawQuery(
`INSERT INTO ${dataSourceSchema}."calendarChannel" (id, "connectedAccountId", "handle", "visibility") VALUES ($1, $2, $3, $4)`,
[
calendarChannel.id,
calendarChannel.connectedAccountId,
calendarChannel.handle,
calendarChannel.visibility,
],
workspaceId,
transactionManager,
);
}
public async getByConnectedAccountId(
connectedAccountId: string,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<ObjectRecord<CalendarChannelWorkspaceEntity>[]> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
return await this.workspaceDataSourceService.executeRawQuery(
`SELECT * FROM ${dataSourceSchema}."calendarChannel" WHERE "connectedAccountId" = $1 LIMIT 1`,
[connectedAccountId],
workspaceId,
transactionManager,
);
}
public async getFirstByConnectedAccountId(
connectedAccountId: string,
workspaceId: string,
): Promise<ObjectRecord<CalendarChannelWorkspaceEntity> | undefined> {
const calendarChannels = await this.getByConnectedAccountId(
connectedAccountId,
workspaceId,
);
return calendarChannels[0];
}
public async getByIds(
ids: string[],
workspaceId: string,
transactionManager?: EntityManager,
): Promise<ObjectRecord<CalendarChannelWorkspaceEntity>[]> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
return await this.workspaceDataSourceService.executeRawQuery(
`SELECT * FROM ${dataSourceSchema}."calendarChannel" WHERE "id" = ANY($1)`,
[ids],
workspaceId,
transactionManager,
);
}
public async getIdsByWorkspaceMemberId(
workspaceMemberId: string,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<ObjectRecord<CalendarChannelWorkspaceEntity>[]> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
const calendarChannelIds =
await this.workspaceDataSourceService.executeRawQuery(
`SELECT "calendarChannel".id FROM ${dataSourceSchema}."calendarChannel" "calendarChannel"
JOIN ${dataSourceSchema}."connectedAccount" ON "calendarChannel"."connectedAccountId" = ${dataSourceSchema}."connectedAccount"."id"
WHERE ${dataSourceSchema}."connectedAccount"."accountOwnerId" = $1`,
[workspaceMemberId],
workspaceId,
transactionManager,
);
return calendarChannelIds;
}
public async updateSyncCursor(
syncCursor: string | null,
calendarChannelId: string,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<void> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
await this.workspaceDataSourceService.executeRawQuery(
`UPDATE ${dataSourceSchema}."calendarChannel" SET "syncCursor" = $1 WHERE "id" = $2`,
[syncCursor || '', calendarChannelId],
workspaceId,
transactionManager,
);
}
}

View File

@ -1,305 +0,0 @@
import { Injectable } from '@nestjs/common';
import { EntityManager } from 'typeorm';
import differenceWith from 'lodash.differencewith';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record';
import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-event-participant.workspace-entity';
import { getFlattenedValuesAndValuesStringForBatchRawQuery } from 'src/modules/calendar/utils/get-flattened-values-and-values-string-for-batch-raw-query.util';
import {
CalendarEventParticipant,
CalendarEventParticipantWithId,
} from 'src/modules/calendar/types/calendar-event';
@Injectable()
export class CalendarEventParticipantRepository {
constructor(
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
) {}
public async getByHandles(
handles: string[],
workspaceId: string,
transactionManager?: EntityManager,
): Promise<ObjectRecord<CalendarEventParticipantWorkspaceEntity>[]> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
return await this.workspaceDataSourceService.executeRawQuery(
`SELECT * FROM ${dataSourceSchema}."calendarEventParticipant" WHERE "handle" = ANY($1)`,
[handles],
workspaceId,
transactionManager,
);
}
public async updateParticipantsPersonId(
participantIds: string[],
personId: string,
workspaceId: string,
transactionManager?: EntityManager,
) {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
await this.workspaceDataSourceService.executeRawQuery(
`UPDATE ${dataSourceSchema}."calendarEventParticipant" SET "personId" = $1 WHERE "id" = ANY($2)`,
[personId, participantIds],
workspaceId,
transactionManager,
);
}
public async updateParticipantsPersonIdAndReturn(
participantIds: string[],
personId: string,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<ObjectRecord<CalendarEventParticipantWorkspaceEntity>[]> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
return await this.workspaceDataSourceService.executeRawQuery(
`UPDATE ${dataSourceSchema}."calendarEventParticipant" SET "personId" = $1 WHERE "id" = ANY($2) RETURNING *`,
[personId, participantIds],
workspaceId,
transactionManager,
);
}
public async updateParticipantsWorkspaceMemberId(
participantIds: string[],
workspaceMemberId: string,
workspaceId: string,
transactionManager?: EntityManager,
) {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
await this.workspaceDataSourceService.executeRawQuery(
`UPDATE ${dataSourceSchema}."calendarEventParticipant" SET "workspaceMemberId" = $1 WHERE "id" = ANY($2)`,
[workspaceMemberId, participantIds],
workspaceId,
transactionManager,
);
}
public async removePersonIdByHandle(
handle: string,
workspaceId: string,
transactionManager?: EntityManager,
) {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
await this.workspaceDataSourceService.executeRawQuery(
`UPDATE ${dataSourceSchema}."calendarEventParticipant" SET "personId" = NULL WHERE "handle" = $1`,
[handle],
workspaceId,
transactionManager,
);
}
public async removeWorkspaceMemberIdByHandle(
handle: string,
workspaceId: string,
transactionManager?: EntityManager,
) {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
await this.workspaceDataSourceService.executeRawQuery(
`UPDATE ${dataSourceSchema}."calendarEventParticipant" SET "workspaceMemberId" = NULL WHERE "handle" = $1`,
[handle],
workspaceId,
transactionManager,
);
}
public async getByIds(
calendarEventParticipantIds: string[],
workspaceId: string,
transactionManager?: EntityManager,
): Promise<ObjectRecord<CalendarEventParticipantWorkspaceEntity>[]> {
if (calendarEventParticipantIds.length === 0) {
return [];
}
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
return await this.workspaceDataSourceService.executeRawQuery(
`SELECT * FROM ${dataSourceSchema}."calendarEventParticipant" WHERE "id" = ANY($1)`,
[calendarEventParticipantIds],
workspaceId,
transactionManager,
);
}
public async getByCalendarEventIds(
calendarEventIds: string[],
workspaceId: string,
transactionManager?: EntityManager,
): Promise<ObjectRecord<CalendarEventParticipantWorkspaceEntity>[]> {
if (calendarEventIds.length === 0) {
return [];
}
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
return await this.workspaceDataSourceService.executeRawQuery(
`SELECT * FROM ${dataSourceSchema}."calendarEventParticipant" WHERE "calendarEventId" = ANY($1)`,
[calendarEventIds],
workspaceId,
transactionManager,
);
}
public async deleteByIds(
calendarEventParticipantIds: string[],
workspaceId: string,
transactionManager?: EntityManager,
): Promise<void> {
if (calendarEventParticipantIds.length === 0) {
return;
}
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
await this.workspaceDataSourceService.executeRawQuery(
`DELETE FROM ${dataSourceSchema}."calendarEventParticipant" WHERE "id" = ANY($1)`,
[calendarEventParticipantIds],
workspaceId,
transactionManager,
);
}
public async updateCalendarEventParticipantsAndReturnNewOnes(
calendarEventParticipants: CalendarEventParticipant[],
workspaceId: string,
transactionManager?: EntityManager,
): Promise<CalendarEventParticipant[]> {
if (calendarEventParticipants.length === 0) {
return [];
}
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
const existingCalendarEventParticipants = await this.getByCalendarEventIds(
calendarEventParticipants.map(
(calendarEventParticipant) => calendarEventParticipant.calendarEventId,
),
workspaceId,
transactionManager,
);
const calendarEventParticipantsToDelete = differenceWith(
existingCalendarEventParticipants,
calendarEventParticipants,
(existingCalendarEventParticipant, calendarEventParticipant) =>
existingCalendarEventParticipant.handle ===
calendarEventParticipant.handle,
);
const newCalendarEventParticipants = differenceWith(
calendarEventParticipants,
existingCalendarEventParticipants,
(calendarEventParticipant, existingCalendarEventParticipant) =>
calendarEventParticipant.handle ===
existingCalendarEventParticipant.handle,
);
await this.deleteByIds(
calendarEventParticipantsToDelete.map(
(calendarEventParticipant) => calendarEventParticipant.id,
),
workspaceId,
transactionManager,
);
const { flattenedValues, valuesString } =
getFlattenedValuesAndValuesStringForBatchRawQuery(
calendarEventParticipants,
{
calendarEventId: 'uuid',
handle: 'text',
displayName: 'text',
isOrganizer: 'boolean',
responseStatus: `${dataSourceSchema}."calendarEventParticipant_responseStatus_enum"`,
},
);
await this.workspaceDataSourceService.executeRawQuery(
`UPDATE ${dataSourceSchema}."calendarEventParticipant" AS "calendarEventParticipant"
SET "displayName" = "newValues"."displayName",
"isOrganizer" = "newValues"."isOrganizer",
"responseStatus" = "newValues"."responseStatus"
FROM (VALUES ${valuesString}) AS "newValues"("calendarEventId", "handle", "displayName", "isOrganizer", "responseStatus")
WHERE "calendarEventParticipant"."handle" = "newValues"."handle"
AND "calendarEventParticipant"."calendarEventId" = "newValues"."calendarEventId"`,
flattenedValues,
workspaceId,
transactionManager,
);
return newCalendarEventParticipants;
}
public async getWithoutPersonIdAndWorkspaceMemberId(
workspaceId: string,
transactionManager?: EntityManager,
): Promise<CalendarEventParticipantWithId[]> {
if (!workspaceId) {
throw new Error('WorkspaceId is required');
}
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
const calendarEventParticipants: CalendarEventParticipantWithId[] =
await this.workspaceDataSourceService.executeRawQuery(
`SELECT "calendarEventParticipant".*
FROM ${dataSourceSchema}."calendarEventParticipant" AS "calendarEventParticipant"
WHERE "calendarEventParticipant"."personId" IS NULL
AND "calendarEventParticipant"."workspaceMemberId" IS NULL`,
[],
workspaceId,
transactionManager,
);
return calendarEventParticipants;
}
public async getByCalendarChannelIdWithoutPersonIdAndWorkspaceMemberId(
calendarChannelId: string,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<CalendarEventParticipantWithId[]> {
if (!workspaceId) {
throw new Error('WorkspaceId is required');
}
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
const calendarEventParticipants: CalendarEventParticipantWithId[] =
await this.workspaceDataSourceService.executeRawQuery(
`SELECT "calendarEventParticipant".*
FROM ${dataSourceSchema}."calendarEventParticipant" AS "calendarEventParticipant"
LEFT JOIN ${dataSourceSchema}."calendarEvent" AS "calendarEvent" ON "calendarEventParticipant"."calendarEventId" = "calendarEvent"."id"
LEFT JOIN ${dataSourceSchema}."calendarChannelEventAssociation" AS "calendarChannelEventAssociation" ON "calendarEvent"."id" = "calendarChannelEventAssociation"."calendarEventId"
WHERE "calendarChannelEventAssociation"."calendarChannelId" = $1
AND "calendarEventParticipant"."personId" IS NULL
AND "calendarEventParticipant"."workspaceMemberId" IS NULL`,
[calendarChannelId],
workspaceId,
transactionManager,
);
return calendarEventParticipants;
}
}

View File

@ -1,227 +0,0 @@
import { Injectable } from '@nestjs/common';
import { EntityManager } from 'typeorm';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record';
import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-event.workspace-entity';
import { getFlattenedValuesAndValuesStringForBatchRawQuery } from 'src/modules/calendar/utils/get-flattened-values-and-values-string-for-batch-raw-query.util';
import { CalendarEvent } from 'src/modules/calendar/types/calendar-event';
import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-event-participant.workspace-entity';
@Injectable()
export class CalendarEventRepository {
constructor(
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
) {}
public async getByIds(
calendarEventIds: string[],
workspaceId: string,
transactionManager?: EntityManager,
): Promise<ObjectRecord<CalendarEventWorkspaceEntity>[]> {
if (calendarEventIds.length === 0) {
return [];
}
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
return await this.workspaceDataSourceService.executeRawQuery(
`SELECT * FROM ${dataSourceSchema}."calendarEvent" WHERE "id" = ANY($1)`,
[calendarEventIds],
workspaceId,
transactionManager,
);
}
public async getByICalUIDs(
iCalUIDs: string[],
workspaceId: string,
transactionManager?: EntityManager,
): Promise<ObjectRecord<CalendarEventWorkspaceEntity>[]> {
if (iCalUIDs.length === 0) {
return [];
}
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
return await this.workspaceDataSourceService.executeRawQuery(
`SELECT * FROM ${dataSourceSchema}."calendarEvent" WHERE "iCalUID" = ANY($1)`,
[iCalUIDs],
workspaceId,
transactionManager,
);
}
public async deleteByIds(
calendarEventIds: string[],
workspaceId: string,
transactionManager?: EntityManager,
): Promise<void> {
if (calendarEventIds.length === 0) {
return;
}
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
await this.workspaceDataSourceService.executeRawQuery(
`DELETE FROM ${dataSourceSchema}."calendarEvent" WHERE "id" = ANY($1)`,
[calendarEventIds],
workspaceId,
transactionManager,
);
}
public async getNonAssociatedCalendarEventIdsPaginated(
limit: number,
offset: number,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<ObjectRecord<CalendarEventParticipantWorkspaceEntity>[]> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
const nonAssociatedCalendarEvents =
await this.workspaceDataSourceService.executeRawQuery(
`SELECT m.id FROM ${dataSourceSchema}."calendarEvent" m
LEFT JOIN ${dataSourceSchema}."calendarChannelEventAssociation" ccea
ON m.id = ccea."calendarEventId"
WHERE ccea.id IS NULL
LIMIT $1 OFFSET $2`,
[limit, offset],
workspaceId,
transactionManager,
);
return nonAssociatedCalendarEvents.map(({ id }) => id);
}
public async getICalUIDCalendarEventIdMap(
iCalUIDs: string[],
workspaceId: string,
transactionManager?: EntityManager,
): Promise<Map<string, string>> {
if (iCalUIDs.length === 0) {
return new Map();
}
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
const calendarEvents:
| {
id: string;
iCalUID: string;
}[]
| undefined = await this.workspaceDataSourceService.executeRawQuery(
`SELECT id, "iCalUID" FROM ${dataSourceSchema}."calendarEvent" WHERE "iCalUID" = ANY($1)`,
[iCalUIDs],
workspaceId,
transactionManager,
);
const iCalUIDsCalendarEventIdsMap = new Map<string, string>();
calendarEvents?.forEach((calendarEvent) => {
iCalUIDsCalendarEventIdsMap.set(calendarEvent.iCalUID, calendarEvent.id);
});
return iCalUIDsCalendarEventIdsMap;
}
public async saveCalendarEvents(
calendarEvents: CalendarEvent[],
workspaceId: string,
transactionManager?: EntityManager,
): Promise<void> {
if (calendarEvents.length === 0) {
return;
}
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
const { flattenedValues, valuesString } =
getFlattenedValuesAndValuesStringForBatchRawQuery(calendarEvents, {
id: 'uuid',
title: 'text',
isCanceled: 'boolean',
isFullDay: 'boolean',
startsAt: 'timestamptz',
endsAt: 'timestamptz',
externalCreatedAt: 'timestamptz',
externalUpdatedAt: 'timestamptz',
description: 'text',
location: 'text',
iCalUID: 'text',
conferenceSolution: 'text',
conferenceLinkLabel: 'text',
conferenceLinkUrl: 'text',
recurringEventExternalId: 'text',
});
await this.workspaceDataSourceService.executeRawQuery(
`INSERT INTO ${dataSourceSchema}."calendarEvent" ("id", "title", "isCanceled", "isFullDay", "startsAt", "endsAt", "externalCreatedAt", "externalUpdatedAt", "description", "location", "iCalUID", "conferenceSolution", "conferenceLinkLabel", "conferenceLinkUrl", "recurringEventExternalId") VALUES ${valuesString}`,
flattenedValues,
workspaceId,
transactionManager,
);
}
public async updateCalendarEvents(
calendarEvents: CalendarEvent[],
workspaceId: string,
transactionManager?: EntityManager,
): Promise<void> {
if (calendarEvents.length === 0) {
return;
}
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
const { flattenedValues, valuesString } =
getFlattenedValuesAndValuesStringForBatchRawQuery(calendarEvents, {
title: 'text',
isCanceled: 'boolean',
isFullDay: 'boolean',
startsAt: 'timestamptz',
endsAt: 'timestamptz',
externalCreatedAt: 'timestamptz',
externalUpdatedAt: 'timestamptz',
description: 'text',
location: 'text',
iCalUID: 'text',
conferenceSolution: 'text',
conferenceLinkLabel: 'text',
conferenceLinkUrl: 'text',
recurringEventExternalId: 'text',
});
await this.workspaceDataSourceService.executeRawQuery(
`UPDATE ${dataSourceSchema}."calendarEvent" AS "calendarEvent"
SET "title" = "newData"."title",
"isCanceled" = "newData"."isCanceled",
"isFullDay" = "newData"."isFullDay",
"startsAt" = "newData"."startsAt",
"endsAt" = "newData"."endsAt",
"externalCreatedAt" = "newData"."externalCreatedAt",
"externalUpdatedAt" = "newData"."externalUpdatedAt",
"description" = "newData"."description",
"location" = "newData"."location",
"conferenceSolution" = "newData"."conferenceSolution",
"conferenceLinkLabel" = "newData"."conferenceLinkLabel",
"conferenceLinkUrl" = "newData"."conferenceLinkUrl",
"recurringEventExternalId" = "newData"."recurringEventExternalId"
FROM (VALUES ${valuesString})
AS "newData"("title", "isCanceled", "isFullDay", "startsAt", "endsAt", "externalCreatedAt", "externalUpdatedAt", "description", "location", "iCalUID", "conferenceSolution", "conferenceLinkLabel", "conferenceLinkUrl", "recurringEventExternalId")
WHERE "calendarEvent"."iCalUID" = "newData"."iCalUID"`,
flattenedValues,
workspaceId,
transactionManager,
);
}
}

View File

@ -1,13 +1,11 @@
import { Module } from '@nestjs/common';
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
import { CalendarEventCleanerService } from 'src/modules/calendar/services/calendar-event-cleaner/calendar-event-cleaner.service';
import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-event.workspace-entity';
@Module({
imports: [
ObjectMetadataRepositoryModule.forFeature([CalendarEventWorkspaceEntity]),
],
imports: [TwentyORMModule.forFeature([CalendarEventWorkspaceEntity])],
providers: [CalendarEventCleanerService],
exports: [CalendarEventCleanerService],
})

View File

@ -1,27 +1,40 @@
import { Injectable } from '@nestjs/common';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { CalendarEventRepository } from 'src/modules/calendar/repositories/calendar-event.repository';
import { Any, IsNull } from 'typeorm';
import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-event.workspace-entity';
import { deleteUsingPagination } from 'src/modules/messaging/message-cleaner/utils/delete-using-pagination.util';
@Injectable()
export class CalendarEventCleanerService {
constructor(
@InjectObjectMetadataRepository(CalendarEventWorkspaceEntity)
private readonly calendarEventRepository: CalendarEventRepository,
@InjectWorkspaceRepository(CalendarEventWorkspaceEntity)
private readonly calendarEventRepository: WorkspaceRepository<CalendarEventWorkspaceEntity>,
) {}
public async cleanWorkspaceCalendarEvents(workspaceId: string) {
await deleteUsingPagination(
workspaceId,
500,
this.calendarEventRepository.getNonAssociatedCalendarEventIdsPaginated.bind(
this.calendarEventRepository,
),
this.calendarEventRepository.deleteByIds.bind(
this.calendarEventRepository,
),
async (limit, offset) => {
const nonAssociatedCalendarEvents =
await this.calendarEventRepository.find({
where: {
calendarChannelEventAssociations: {
id: IsNull(),
},
},
take: limit,
skip: offset,
});
return nonAssociatedCalendarEvents.map(({ id }) => id);
},
async (ids) => {
await this.calendarEventRepository.delete({ id: Any(ids) });
},
);
}
}

View File

@ -1,14 +1,17 @@
import { Module } from '@nestjs/common';
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { AddPersonIdAndWorkspaceMemberIdModule } from 'src/modules/calendar-messaging-participant/services/add-person-id-and-workspace-member-id/add-person-id-and-workspace-member-id.module';
import { CalendarEventParticipantService } from 'src/modules/calendar/services/calendar-event-participant/calendar-event-participant.service';
import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-event-participant.workspace-entity';
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
@Module({
imports: [
WorkspaceDataSourceModule,
TwentyORMModule.forFeature([CalendarEventParticipantWorkspaceEntity]),
ObjectMetadataRepositoryModule.forFeature([PersonWorkspaceEntity]),
AddPersonIdAndWorkspaceMemberIdModule,
],

View File

@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { EntityManager } from 'typeorm';
import { Any, EntityManager } from 'typeorm';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { PersonRepository } from 'src/modules/person/repositories/person.repository';
@ -9,17 +9,18 @@ import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/perso
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { getFlattenedValuesAndValuesStringForBatchRawQuery } from 'src/modules/calendar/utils/get-flattened-values-and-values-string-for-batch-raw-query.util';
import { CalendarEventParticipant } from 'src/modules/calendar/types/calendar-event';
import { CalendarEventParticipantRepository } from 'src/modules/calendar/repositories/calendar-event-participant.repository';
import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-event-participant.workspace-entity';
import { AddPersonIdAndWorkspaceMemberIdService } from 'src/modules/calendar-messaging-participant/services/add-person-id-and-workspace-member-id/add-person-id-and-workspace-member-id.service';
import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record';
import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
@Injectable()
export class CalendarEventParticipantService {
constructor(
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
@InjectObjectMetadataRepository(CalendarEventParticipantWorkspaceEntity)
private readonly calendarEventParticipantRepository: CalendarEventParticipantRepository,
@InjectWorkspaceRepository(CalendarEventParticipantWorkspaceEntity)
private readonly calendarEventParticipantRepository: WorkspaceRepository<CalendarEventParticipantWorkspaceEntity>,
@InjectObjectMetadataRepository(PersonWorkspaceEntity)
private readonly personRepository: PersonRepository,
private readonly addPersonIdAndWorkspaceMemberIdService: AddPersonIdAndWorkspaceMemberIdService,
@ -31,11 +32,11 @@ export class CalendarEventParticipantService {
workspaceId: string,
transactionManager?: EntityManager,
): Promise<ObjectRecord<CalendarEventParticipantWorkspaceEntity>[]> {
const participants =
await this.calendarEventParticipantRepository.getByHandles(
createdPeople.map((person) => person.email),
workspaceId,
);
const participants = await this.calendarEventParticipantRepository.find({
where: {
handle: Any(createdPeople.map((person) => person.email)),
},
});
if (!participants) return [];
@ -132,33 +133,50 @@ export class CalendarEventParticipantService {
workspaceMemberId?: string,
) {
const calendarEventParticipantsToUpdate =
await this.calendarEventParticipantRepository.getByHandles(
[email],
workspaceId,
);
await this.calendarEventParticipantRepository.find({
where: {
handle: email,
},
});
const calendarEventParticipantIdsToUpdate =
calendarEventParticipantsToUpdate.map((participant) => participant.id);
if (personId) {
await this.calendarEventParticipantRepository.update(
{
id: Any(calendarEventParticipantIdsToUpdate),
},
{
person: {
id: personId,
},
},
);
const updatedCalendarEventParticipants =
await this.calendarEventParticipantRepository.updateParticipantsPersonIdAndReturn(
calendarEventParticipantIdsToUpdate,
personId,
workspaceId,
);
await this.calendarEventParticipantRepository.find({
where: {
id: Any(calendarEventParticipantIdsToUpdate),
},
});
this.eventEmitter.emit(`calendarEventParticipant.matched`, {
workspaceId,
userId: null,
workspaceMemberId: null,
calendarEventParticipants: updatedCalendarEventParticipants,
});
}
if (workspaceMemberId) {
await this.calendarEventParticipantRepository.updateParticipantsWorkspaceMemberId(
calendarEventParticipantIdsToUpdate,
workspaceMemberId,
workspaceId,
await this.calendarEventParticipantRepository.update(
{
id: Any(calendarEventParticipantIdsToUpdate),
},
{
workspaceMember: {
id: workspaceMemberId,
},
},
);
}
}
@ -170,15 +188,23 @@ export class CalendarEventParticipantService {
workspaceMemberId?: string,
) {
if (personId) {
await this.calendarEventParticipantRepository.removePersonIdByHandle(
handle,
workspaceId,
await this.calendarEventParticipantRepository.update(
{
handle,
},
{
person: null,
},
);
}
if (workspaceMemberId) {
await this.calendarEventParticipantRepository.removeWorkspaceMemberIdByHandle(
handle,
workspaceId,
await this.calendarEventParticipantRepository.update(
{
handle,
},
{
workspaceMember: null,
},
);
}
}

View File

@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { CalendarEventCleanerModule } from 'src/modules/calendar/services/calendar-event-cleaner/calendar-event-cleaner.module';
import { CalendarEventParticipantModule } from 'src/modules/calendar/services/calendar-event-participant/calendar-event-participant.module';
@ -20,12 +21,14 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
@Module({
imports: [
CalendarProvidersModule,
ObjectMetadataRepositoryModule.forFeature([
ConnectedAccountWorkspaceEntity,
TwentyORMModule.forFeature([
CalendarEventWorkspaceEntity,
CalendarChannelWorkspaceEntity,
CalendarChannelEventAssociationWorkspaceEntity,
CalendarEventParticipantWorkspaceEntity,
]),
ObjectMetadataRepositoryModule.forFeature([
ConnectedAccountWorkspaceEntity,
BlocklistWorkspaceEntity,
PersonWorkspaceEntity,
WorkspaceMemberWorkspaceEntity,

View File

@ -2,7 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Repository } from 'typeorm';
import { Any, Repository } from 'typeorm';
import { calendar_v3 as calendarV3 } from 'googleapis';
import { GaxiosError } from 'gaxios';
@ -13,12 +13,7 @@ import {
FeatureFlagKeys,
} from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { GoogleCalendarClientProvider } from 'src/modules/calendar/services/providers/google-calendar/google-calendar.provider';
import { CalendarChannelEventAssociationRepository } from 'src/modules/calendar/repositories/calendar-channel-event-association.repository';
import { CalendarChannelRepository } from 'src/modules/calendar/repositories/calendar-channel.repository';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { CalendarEventRepository } from 'src/modules/calendar/repositories/calendar-event.repository';
import { formatGoogleCalendarEvent } from 'src/modules/calendar/utils/format-google-calendar-event.util';
import { CalendarEventParticipantRepository } from 'src/modules/calendar/repositories/calendar-event-participant.repository';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-event.workspace-entity';
@ -28,7 +23,10 @@ import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/st
import { BlocklistWorkspaceEntity } from 'src/modules/connected-account/standard-objects/blocklist.workspace-entity';
import { CalendarEventCleanerService } from 'src/modules/calendar/services/calendar-event-cleaner/calendar-event-cleaner.service';
import { CalendarEventParticipantService } from 'src/modules/calendar/services/calendar-event-participant/calendar-event-participant.service';
import { CalendarEventWithParticipants } from 'src/modules/calendar/types/calendar-event';
import {
CalendarEventParticipant,
CalendarEventWithParticipants,
} from 'src/modules/calendar/types/calendar-event';
import { filterOutBlocklistedEvents } from 'src/modules/calendar/utils/filter-out-blocklisted-events.util';
import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator';
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
@ -37,7 +35,12 @@ import {
CreateCompanyAndContactJob,
CreateCompanyAndContactJobData,
} from 'src/modules/connected-account/auto-companies-and-contacts-creation/jobs/create-company-and-contact.job';
import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record';
import { isDefined } from 'src/utils/is-defined';
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 GoogleCalendarSyncService {
@ -47,21 +50,20 @@ export class GoogleCalendarSyncService {
private readonly googleCalendarClientProvider: GoogleCalendarClientProvider,
@InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity)
private readonly connectedAccountRepository: ConnectedAccountRepository,
@InjectObjectMetadataRepository(CalendarEventWorkspaceEntity)
private readonly calendarEventRepository: CalendarEventRepository,
@InjectObjectMetadataRepository(CalendarChannelWorkspaceEntity)
private readonly calendarChannelRepository: CalendarChannelRepository,
@InjectObjectMetadataRepository(
CalendarChannelEventAssociationWorkspaceEntity,
)
private readonly calendarChannelEventAssociationRepository: CalendarChannelEventAssociationRepository,
@InjectObjectMetadataRepository(CalendarEventParticipantWorkspaceEntity)
private readonly calendarEventParticipantsRepository: CalendarEventParticipantRepository,
@InjectWorkspaceRepository(CalendarEventWorkspaceEntity)
private readonly calendarEventRepository: WorkspaceRepository<CalendarEventWorkspaceEntity>,
@InjectWorkspaceRepository(CalendarChannelWorkspaceEntity)
private readonly calendarChannelRepository: WorkspaceRepository<CalendarChannelWorkspaceEntity>,
@InjectWorkspaceRepository(CalendarChannelEventAssociationWorkspaceEntity)
private readonly calendarChannelEventAssociationRepository: WorkspaceRepository<CalendarChannelEventAssociationWorkspaceEntity>,
@InjectWorkspaceRepository(CalendarEventParticipantWorkspaceEntity)
private readonly calendarEventParticipantsRepository: WorkspaceRepository<CalendarEventParticipantWorkspaceEntity>,
@InjectObjectMetadataRepository(BlocklistWorkspaceEntity)
private readonly blocklistRepository: BlocklistRepository,
@InjectRepository(FeatureFlagEntity, 'core')
private readonly featureFlagRepository: Repository<FeatureFlagEntity>,
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
@InjectWorkspaceDatasource()
private readonly workspaceDataSource: WorkspaceDataSource,
private readonly calendarEventCleanerService: CalendarEventCleanerService,
private readonly calendarEventParticipantsService: CalendarEventParticipantService,
@InjectMessageQueue(MessageQueue.contactCreationQueue)
@ -92,11 +94,11 @@ export class GoogleCalendarSyncService {
);
}
const calendarChannel =
await this.calendarChannelRepository.getFirstByConnectedAccountId(
connectedAccountId,
workspaceId,
);
const calendarChannel = await this.calendarChannelRepository.findOneBy({
connectedAccount: {
id: connectedAccountId,
},
});
const syncToken = calendarChannel?.syncCursor || undefined;
@ -122,6 +124,12 @@ export class GoogleCalendarSyncService {
return;
}
if (!workspaceMemberId) {
throw new Error(
`Workspace member ID is undefined for connected account ${connectedAccountId} in workspace ${workspaceId}`,
);
}
const blocklist = await this.getBlocklist(workspaceMemberId, workspaceId);
let filteredEvents = filterOutBlocklistedEvents(
@ -143,11 +151,18 @@ export class GoogleCalendarSyncService {
.filter((event) => event.status === 'cancelled')
.map((event) => event.id as string);
const iCalUIDCalendarEventIdMap =
await this.calendarEventRepository.getICalUIDCalendarEventIdMap(
filteredEvents.map((calendarEvent) => calendarEvent.iCalUID as string),
workspaceId,
);
const existingCalendarEvents = await this.calendarEventRepository.find({
where: {
iCalUID: Any(filteredEvents.map((event) => event.iCalUID as string)),
},
});
const iCalUIDCalendarEventIdMap = new Map(
existingCalendarEvents.map((calendarEvent) => [
calendarEvent.iCalUID,
calendarEvent.id,
]),
);
const formattedEvents = filteredEvents.map((event) =>
formatGoogleCalendarEvent(event, iCalUIDCalendarEventIdMap),
@ -157,31 +172,34 @@ export class GoogleCalendarSyncService {
let startTime = Date.now();
const existingEvents = await this.calendarEventRepository.getByICalUIDs(
formattedEvents.map((event) => event.iCalUID),
workspaceId,
const existingEventsICalUIDs = existingCalendarEvents.map(
(calendarEvent) => calendarEvent.iCalUID,
);
const existingEventsICalUIDs = existingEvents.map((event) => event.iCalUID);
let endTime = Date.now();
const eventsToSave = formattedEvents.filter(
(event) => !existingEventsICalUIDs.includes(event.iCalUID),
(calendarEvent) =>
!existingEventsICalUIDs.includes(calendarEvent.iCalUID),
);
const eventsToUpdate = formattedEvents.filter((event) =>
existingEventsICalUIDs.includes(event.iCalUID),
const eventsToUpdate = formattedEvents.filter((calendarEvent) =>
existingEventsICalUIDs.includes(calendarEvent.iCalUID),
);
startTime = Date.now();
const existingCalendarChannelEventAssociations =
await this.calendarChannelEventAssociationRepository.getByEventExternalIdsAndCalendarChannelId(
formattedEvents.map((event) => event.externalId),
calendarChannelId,
workspaceId,
);
await this.calendarChannelEventAssociationRepository.find({
where: {
eventExternalId: Any(
formattedEvents.map((calendarEvent) => calendarEvent.id),
),
calendarChannel: {
id: calendarChannelId,
},
},
});
endTime = Date.now();
@ -193,14 +211,14 @@ export class GoogleCalendarSyncService {
const calendarChannelEventAssociationsToSave = formattedEvents
.filter(
(event) =>
(calendarEvent) =>
!existingCalendarChannelEventAssociations.some(
(association) => association.eventExternalId === event.id,
(association) => association.eventExternalId === calendarEvent.id,
),
)
.map((event) => ({
calendarEventId: event.id,
eventExternalId: event.externalId,
.map((calendarEvent) => ({
calendarEventId: calendarEvent.id,
eventExternalId: calendarEvent.externalId,
calendarChannelId,
}));
@ -216,11 +234,12 @@ export class GoogleCalendarSyncService {
startTime = Date.now();
await this.calendarChannelEventAssociationRepository.deleteByEventExternalIdsAndCalendarChannelId(
cancelledEventExternalIds,
calendarChannelId,
workspaceId,
);
await this.calendarChannelEventAssociationRepository.delete({
eventExternalId: Any(cancelledEventExternalIds),
calendarChannel: {
id: calendarChannelId,
},
});
endTime = Date.now();
@ -257,10 +276,13 @@ export class GoogleCalendarSyncService {
startTime = Date.now();
await this.calendarChannelRepository.updateSyncCursor(
nextSyncToken,
calendarChannel.id,
workspaceId,
await this.calendarChannelRepository.update(
{
id: calendarChannel.id,
},
{
syncCursor: nextSyncToken,
},
);
endTime = Date.now();
@ -337,10 +359,13 @@ export class GoogleCalendarSyncService {
throw error;
}
await this.calendarChannelRepository.updateSyncCursor(
null,
connectedAccountId,
workspaceId,
await this.calendarChannelRepository.update(
{
id: connectedAccountId,
},
{
syncCursor: '',
},
);
this.logger.log(
@ -395,11 +420,6 @@ export class GoogleCalendarSyncService {
calendarChannel: CalendarChannelWorkspaceEntity,
workspaceId: string,
): Promise<void> {
const dataSourceMetadata =
await this.workspaceDataSourceService.connectToWorkspaceDataSource(
workspaceId,
);
const participantsToSave = eventsToSave.flatMap(
(event) => event.participants,
);
@ -415,103 +435,154 @@ export class GoogleCalendarSyncService {
[];
try {
await dataSourceMetadata?.transaction(async (transactionManager) => {
startTime = Date.now();
await this.workspaceDataSource?.transaction(
async (transactionManager) => {
startTime = Date.now();
await this.calendarEventRepository.saveCalendarEvents(
eventsToSave,
workspaceId,
transactionManager,
);
await this.calendarEventRepository.save(
eventsToSave,
{},
transactionManager,
);
endTime = Date.now();
endTime = Date.now();
this.logger.log(
`google calendar sync for workspace ${workspaceId} and account ${
connectedAccount.id
}: saving ${eventsToSave.length} events in ${endTime - startTime}ms.`,
);
this.logger.log(
`google calendar sync for workspace ${workspaceId} and account ${
connectedAccount.id
}: saving ${eventsToSave.length} events in ${
endTime - startTime
}ms.`,
);
startTime = Date.now();
startTime = Date.now();
await this.calendarEventRepository.updateCalendarEvents(
eventsToUpdate,
workspaceId,
transactionManager,
);
await this.calendarChannelRepository.save(
eventsToUpdate,
{},
transactionManager,
);
endTime = Date.now();
endTime = Date.now();
this.logger.log(
`google calendar sync for workspace ${workspaceId} and account ${
connectedAccount.id
}: updating ${eventsToUpdate.length} events in ${
endTime - startTime
}ms.`,
);
this.logger.log(
`google calendar sync for workspace ${workspaceId} and account ${
connectedAccount.id
}: updating ${eventsToUpdate.length} events in ${
endTime - startTime
}ms.`,
);
startTime = Date.now();
startTime = Date.now();
await this.calendarChannelEventAssociationRepository.saveCalendarChannelEventAssociations(
calendarChannelEventAssociationsToSave,
workspaceId,
transactionManager,
);
await this.calendarChannelEventAssociationRepository.save(
calendarChannelEventAssociationsToSave,
{},
transactionManager,
);
endTime = Date.now();
endTime = Date.now();
this.logger.log(
`google calendar sync for workspace ${workspaceId} and account ${
connectedAccount.id
}: saving calendar channel event associations in ${
endTime - startTime
}ms.`,
);
this.logger.log(
`google calendar sync for workspace ${workspaceId} and account ${
connectedAccount.id
}: saving calendar channel event associations in ${
endTime - startTime
}ms.`,
);
startTime = Date.now();
startTime = Date.now();
const newCalendarEventParticipants =
await this.calendarEventParticipantsRepository.updateCalendarEventParticipantsAndReturnNewOnes(
const existingCalendarEventParticipants =
await this.calendarEventParticipantsRepository.find({
where: {
calendarEvent: {
id: Any(
participantsToUpdate
.map((participant) => participant.calendarEventId)
.filter(isDefined),
),
},
},
});
const {
calendarEventParticipantsToDelete,
newCalendarEventParticipants,
} = participantsToUpdate.reduce(
(acc, calendarEventParticipant) => {
const existingCalendarEventParticipant =
existingCalendarEventParticipants.find(
(existingCalendarEventParticipant) =>
existingCalendarEventParticipant.handle ===
calendarEventParticipant.handle,
);
if (existingCalendarEventParticipant) {
acc.calendarEventParticipantsToDelete.push(
existingCalendarEventParticipant,
);
} else {
acc.newCalendarEventParticipants.push(calendarEventParticipant);
}
return acc;
},
{
calendarEventParticipantsToDelete:
[] as CalendarEventParticipantWorkspaceEntity[],
newCalendarEventParticipants: [] as CalendarEventParticipant[],
},
);
await this.calendarEventParticipantsRepository.delete({
id: Any(
calendarEventParticipantsToDelete.map(
(calendarEventParticipant) => calendarEventParticipant.id,
),
),
});
await this.calendarEventParticipantsRepository.save(
participantsToUpdate,
workspaceId,
transactionManager,
);
endTime = Date.now();
endTime = Date.now();
participantsToSave.push(...newCalendarEventParticipants);
participantsToSave.push(...newCalendarEventParticipants);
this.logger.log(
`google calendar sync for workspace ${workspaceId} and account ${
connectedAccount.id
}: updating participants in ${endTime - startTime}ms.`,
);
startTime = Date.now();
const savedCalendarEventParticipants =
await this.calendarEventParticipantsService.saveCalendarEventParticipants(
participantsToSave,
workspaceId,
transactionManager,
this.logger.log(
`google calendar sync for workspace ${workspaceId} and account ${
connectedAccount.id
}: updating participants in ${endTime - startTime}ms.`,
);
savedCalendarEventParticipantsToEmit.push(
...savedCalendarEventParticipants,
);
startTime = Date.now();
endTime = Date.now();
const savedCalendarEventParticipants =
await this.calendarEventParticipantsService.saveCalendarEventParticipants(
participantsToSave,
workspaceId,
transactionManager,
);
this.logger.log(
`google calendar sync for workspace ${workspaceId} and account ${
connectedAccount.id
}: saving participants in ${endTime - startTime}ms.`,
);
});
savedCalendarEventParticipantsToEmit.push(
...savedCalendarEventParticipants,
);
endTime = Date.now();
this.logger.log(
`google calendar sync for workspace ${workspaceId} and account ${
connectedAccount.id
}: saving participants in ${endTime - startTime}ms.`,
);
},
);
this.eventEmitter.emit(`calendarEventParticipant.matched`, {
workspaceId,
userId: connectedAccount.accountOwnerId,
workspaceMemberId: connectedAccount.accountOwnerId,
calendarEventParticipants: savedCalendarEventParticipantsToEmit,
});

View File

@ -1,13 +1,11 @@
import { Module } from '@nestjs/common';
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
import { WorkspaceGoogleCalendarSyncService } from 'src/modules/calendar/services/workspace-google-calendar-sync/workspace-google-calendar-sync.service';
import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-channel.workspace-entity';
@Module({
imports: [
ObjectMetadataRepositoryModule.forFeature([CalendarChannelWorkspaceEntity]),
],
imports: [TwentyORMModule.forFeature([CalendarChannelWorkspaceEntity])],
providers: [WorkspaceGoogleCalendarSyncService],
exports: [WorkspaceGoogleCalendarSyncService],
})

View File

@ -3,19 +3,19 @@ import { Injectable } from '@nestjs/common';
import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator';
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import {
GoogleCalendarSyncJobData,
GoogleCalendarSyncJob,
} from 'src/modules/calendar/jobs/google-calendar-sync.job';
import { CalendarChannelRepository } from 'src/modules/calendar/repositories/calendar-channel.repository';
import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-channel.workspace-entity';
@Injectable()
export class WorkspaceGoogleCalendarSyncService {
constructor(
@InjectObjectMetadataRepository(CalendarChannelWorkspaceEntity)
private readonly calendarChannelRepository: CalendarChannelRepository,
@InjectWorkspaceRepository(CalendarChannelWorkspaceEntity)
private readonly calendarChannelRepository: WorkspaceRepository<CalendarChannelWorkspaceEntity>,
@InjectMessageQueue(MessageQueue.calendarQueue)
private readonly messageQueueService: MessageQueueService,
) {}
@ -23,8 +23,7 @@ export class WorkspaceGoogleCalendarSyncService {
public async startWorkspaceGoogleCalendarSync(
workspaceId: string,
): Promise<void> {
const calendarChannels =
await this.calendarChannelRepository.getAll(workspaceId);
const calendarChannels = await this.calendarChannelRepository.find({});
for (const calendarChannel of calendarChannels) {
if (!calendarChannel?.isSyncEnabled) {
@ -35,7 +34,7 @@ export class WorkspaceGoogleCalendarSyncService {
GoogleCalendarSyncJob.name,
{
workspaceId,
connectedAccountId: calendarChannel.connectedAccountId,
connectedAccountId: calendarChannel.connectedAccount.id,
},
{
retryLimit: 2,

View File

@ -120,7 +120,7 @@ export class CalendarEventParticipantWorkspaceEntity extends BaseWorkspaceEntity
inverseSideFieldKey: 'calendarEventParticipants',
})
@WorkspaceIsNullable()
person: Relation<PersonWorkspaceEntity>;
person: Relation<PersonWorkspaceEntity> | null;
@WorkspaceRelation({
standardId: CALENDAR_EVENT_PARTICIPANT_STANDARD_FIELD_IDS.workspaceMember,
@ -133,5 +133,5 @@ export class CalendarEventParticipantWorkspaceEntity extends BaseWorkspaceEntity
inverseSideFieldKey: 'calendarEventParticipants',
})
@WorkspaceIsNullable()
workspaceMember: Relation<WorkspaceMemberWorkspaceEntity>;
workspaceMember: Relation<WorkspaceMemberWorkspaceEntity> | null;
}

View File

@ -68,7 +68,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconUsers',
})
@WorkspaceIsNullable()
employees: number;
employees: number | null;
@WorkspaceField({
standardId: COMPANY_STANDARD_FIELD_IDS.linkedinLink,
@ -78,7 +78,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconBrandLinkedin',
})
@WorkspaceIsNullable()
linkedinLink: LinkMetadata;
linkedinLink: LinkMetadata | null;
@WorkspaceField({
standardId: COMPANY_STANDARD_FIELD_IDS.xLink,
@ -88,7 +88,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconBrandX',
})
@WorkspaceIsNullable()
xLink: LinkMetadata;
xLink: LinkMetadata | null;
@WorkspaceField({
standardId: COMPANY_STANDARD_FIELD_IDS.annualRecurringRevenue,
@ -99,7 +99,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconMoneybag',
})
@WorkspaceIsNullable()
annualRecurringRevenue: CurrencyMetadata;
annualRecurringRevenue: CurrencyMetadata | null;
@WorkspaceField({
standardId: COMPANY_STANDARD_FIELD_IDS.idealCustomerProfile,
@ -121,7 +121,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity {
})
@WorkspaceIsSystem()
@WorkspaceIsNullable()
position: number;
position: number | null;
// Relations
@WorkspaceRelation({
@ -149,7 +149,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity {
onDelete: RelationOnDeleteAction.SET_NULL,
})
@WorkspaceIsNullable()
accountOwner: Relation<WorkspaceMemberWorkspaceEntity>;
accountOwner: Relation<WorkspaceMemberWorkspaceEntity> | null;
@WorkspaceRelation({
standardId: COMPANY_STANDARD_FIELD_IDS.activityTargets,

View File

@ -8,7 +8,6 @@ import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repos
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-event-participant.workspace-entity';
import { CalendarEventParticipantModule } from 'src/modules/calendar/services/calendar-event-participant/calendar-event-participant.module';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module';
@ -20,7 +19,6 @@ import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-co
ObjectMetadataRepositoryModule.forFeature([
PersonWorkspaceEntity,
WorkspaceMemberWorkspaceEntity,
CalendarEventParticipantWorkspaceEntity,
]),
MessagingCommonModule,
WorkspaceDataSourceModule,

View File

@ -1,13 +1,12 @@
import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record';
import { CreateCompanyAndContactService } from 'src/modules/connected-account/auto-companies-and-contacts-creation/services/create-company-and-contact.service';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
export type CreateCompanyAndContactJobData = {
workspaceId: string;
connectedAccount: ObjectRecord<ConnectedAccountWorkspaceEntity>;
connectedAccount: ConnectedAccountWorkspaceEntity;
contactsToCreate: {
displayName: string;
handle: string;

View File

@ -15,14 +15,15 @@ import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/perso
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { getUniqueContactsAndHandles } from 'src/modules/connected-account/auto-companies-and-contacts-creation/utils/get-unique-contacts-and-handles.util';
import { Contacts } from 'src/modules/connected-account/auto-companies-and-contacts-creation/types/contact.type';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { CalendarEventParticipantService } from 'src/modules/calendar/services/calendar-event-participant/calendar-event-participant.service';
import { filterOutContactsFromCompanyOrWorkspace } from 'src/modules/connected-account/auto-companies-and-contacts-creation/utils/filter-out-contacts-from-company-or-workspace.util';
import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record';
import { MessagingMessageParticipantService } from 'src/modules/messaging/common/services/messaging-message-participant.service';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity';
import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-event-participant.workspace-entity';
import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
import { InjectWorkspaceDatasource } from 'src/engine/twenty-orm/decorators/inject-workspace-datasource.decorator';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
@Injectable()
export class CreateCompanyAndContactService {
@ -33,7 +34,8 @@ export class CreateCompanyAndContactService {
private readonly personRepository: PersonRepository,
@InjectObjectMetadataRepository(WorkspaceMemberWorkspaceEntity)
private readonly workspaceMemberRepository: WorkspaceMemberRepository,
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
@InjectWorkspaceDatasource()
private readonly workspaceDataSource: WorkspaceDataSource,
private readonly messageParticipantService: MessagingMessageParticipantService,
private readonly calendarEventParticipantService: CalendarEventParticipantService,
private readonly eventEmitter: EventEmitter2,
@ -130,21 +132,16 @@ export class CreateCompanyAndContactService {
}
async createCompaniesAndContactsAndUpdateParticipants(
connectedAccount: ObjectRecord<ConnectedAccountWorkspaceEntity>,
connectedAccount: ConnectedAccountWorkspaceEntity,
contactsToCreate: Contacts,
workspaceId: string,
) {
const { dataSource: workspaceDataSource } =
await this.workspaceDataSourceService.connectedToWorkspaceDataSourceAndReturnMetadata(
workspaceId,
);
let updatedMessageParticipants: ObjectRecord<MessageParticipantWorkspaceEntity>[] =
[];
let updatedCalendarEventParticipants: ObjectRecord<CalendarEventParticipantWorkspaceEntity>[] =
[];
await workspaceDataSource?.transaction(
await this.workspaceDataSource?.transaction(
async (transactionManager: EntityManager) => {
const createdPeople = await this.createCompaniesAndPeople(
connectedAccount.handle,
@ -171,13 +168,13 @@ export class CreateCompanyAndContactService {
this.eventEmitter.emit(`messageParticipant.matched`, {
workspaceId,
userId: connectedAccount.accountOwnerId,
workspaceMemberId: connectedAccount.accountOwnerId,
messageParticipants: updatedMessageParticipants,
});
this.eventEmitter.emit(`calendarEventParticipant.matched`, {
workspaceId,
userId: connectedAccount.accountOwnerId,
workspaceMemberId: connectedAccount.accountOwnerId,
calendarEventParticipants: updatedCalendarEventParticipants,
});
}

View File

@ -80,6 +80,12 @@ export class GoogleAPIRefreshAccessTokenService {
workspaceId,
);
if (!messageChannel.connectedAccountId) {
throw new Error(
`No connected account ID found for message channel ${messageChannel.id} in workspace ${workspaceId}`,
);
}
await this.connectedAccountRepository.updateAuthFailedAt(
messageChannel.connectedAccountId,
workspaceId,

View File

@ -86,7 +86,7 @@ export class ConnectedAccountWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconX',
})
@WorkspaceIsNullable()
authFailedAt: Date;
authFailedAt: Date | null;
@WorkspaceRelation({
standardId: CONNECTED_ACCOUNT_STANDARD_FIELD_IDS.accountOwner,
@ -100,6 +100,8 @@ export class ConnectedAccountWorkspaceEntity extends BaseWorkspaceEntity {
})
accountOwner: Relation<WorkspaceMemberWorkspaceEntity>;
accountOwnerId: string;
@WorkspaceRelation({
standardId: CONNECTED_ACCOUNT_STANDARD_FIELD_IDS.messageChannels,
type: RelationMetadataType.ONE_TO_MANY,

View File

@ -63,7 +63,7 @@ export class FavoriteWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideFieldKey: 'favorites',
})
@WorkspaceIsNullable()
person: Relation<PersonWorkspaceEntity>;
person: Relation<PersonWorkspaceEntity> | null;
@WorkspaceRelation({
standardId: FAVORITE_STANDARD_FIELD_IDS.company,
@ -76,7 +76,7 @@ export class FavoriteWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideFieldKey: 'favorites',
})
@WorkspaceIsNullable()
company: Relation<CompanyWorkspaceEntity>;
company: Relation<CompanyWorkspaceEntity> | null;
@WorkspaceRelation({
standardId: FAVORITE_STANDARD_FIELD_IDS.opportunity,
@ -89,7 +89,7 @@ export class FavoriteWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideFieldKey: 'favorites',
})
@WorkspaceIsNullable()
opportunity: Relation<OpportunityWorkspaceEntity>;
opportunity: Relation<OpportunityWorkspaceEntity> | null;
@WorkspaceDynamicRelation({
type: RelationMetadataType.MANY_TO_ONE,

View File

@ -56,6 +56,12 @@ export class BlocklistItemDeleteMessagesJob {
`Deleting messages from ${handle} in workspace ${workspaceId} for workspace member ${workspaceMemberId}`,
);
if (!workspaceMemberId) {
throw new Error(
`Workspace member ID is not defined for blocklist item ${blocklistItemId} in workspace ${workspaceId}`,
);
}
const messageChannels =
await this.messageChannelRepository.getIdsByWorkspaceMemberId(
workspaceMemberId,

View File

@ -9,6 +9,7 @@ import { MessageChannelRepository } from 'src/modules/messaging/common/repositor
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
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 { isDefined } from 'src/utils/is-defined';
export class CanAccessMessageThreadService {
constructor(
@ -46,7 +47,9 @@ export class CanAccessMessageThreadService {
const messageChannelsConnectedAccounts =
await this.connectedAccountRepository.getByIds(
messageChannels.map((channel) => channel.connectedAccountId),
messageChannels
.map((channel) => channel.connectedAccountId)
.filter(isDefined),
workspaceId,
);

View File

@ -211,6 +211,12 @@ export class MessagingErrorHandlingService {
workspaceId,
);
if (!messageChannel.connectedAccountId) {
throw new Error(
`Connected account ID is not defined for message channel ${messageChannel.id} in workspace ${workspaceId}`,
);
}
await this.connectedAccountRepository.updateAuthFailedAt(
messageChannel.connectedAccountId,
workspaceId,

View File

@ -149,7 +149,7 @@ export class MessagingMessageParticipantService {
this.eventEmitter.emit(`messageParticipant.matched`, {
workspaceId,
userId: null,
workspaceMemberId: null,
messageParticipants: updatedMessageParticipants,
});
}

View File

@ -107,7 +107,7 @@ export class MessagingSaveMessagesAndEnqueueContactCreationService {
this.eventEmitter.emit(`messageParticipant.matched`, {
workspaceId,
userId: connectedAccount.accountOwnerId,
workspaceMemberId: connectedAccount.accountOwnerId,
messageParticipants: savedMessageParticipants,
});

View File

@ -35,7 +35,7 @@ export class MessageChannelMessageAssociationWorkspaceEntity extends BaseWorkspa
icon: 'IconHash',
})
@WorkspaceIsNullable()
messageExternalId: string;
messageExternalId: string | null;
@WorkspaceField({
standardId:
@ -46,7 +46,7 @@ export class MessageChannelMessageAssociationWorkspaceEntity extends BaseWorkspa
icon: 'IconHash',
})
@WorkspaceIsNullable()
messageThreadExternalId: string;
messageThreadExternalId: string | null;
@WorkspaceRelation({
standardId:
@ -60,7 +60,7 @@ export class MessageChannelMessageAssociationWorkspaceEntity extends BaseWorkspa
inverseSideFieldKey: 'messageChannelMessageAssociations',
})
@WorkspaceIsNullable()
messageChannel: Relation<MessageChannelWorkspaceEntity>;
messageChannel: Relation<MessageChannelWorkspaceEntity> | null;
@WorkspaceRelation({
standardId: MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_STANDARD_FIELD_IDS.message,
@ -73,7 +73,7 @@ export class MessageChannelMessageAssociationWorkspaceEntity extends BaseWorkspa
inverseSideFieldKey: 'messageChannelMessageAssociations',
})
@WorkspaceIsNullable()
message: Relation<MessageWorkspaceEntity>;
message: Relation<MessageWorkspaceEntity> | null;
@WorkspaceRelation({
standardId:
@ -87,5 +87,5 @@ export class MessageChannelMessageAssociationWorkspaceEntity extends BaseWorkspa
inverseSideFieldKey: 'messageChannelMessageAssociations',
})
@WorkspaceIsNullable()
messageThread: Relation<MessageThreadWorkspaceEntity>;
messageThread: Relation<MessageThreadWorkspaceEntity> | null;
}

View File

@ -162,7 +162,7 @@ export class MessageChannelWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconHistory',
})
@WorkspaceIsNullable()
syncedAt: string;
syncedAt: string | null;
@WorkspaceField({
standardId: MESSAGE_CHANNEL_STANDARD_FIELD_IDS.syncStatus,
@ -224,7 +224,7 @@ export class MessageChannelWorkspaceEntity extends BaseWorkspaceEntity {
],
})
@WorkspaceIsNullable()
syncStatus: MessageChannelSyncStatus;
syncStatus: MessageChannelSyncStatus | null;
@WorkspaceField({
standardId: MESSAGE_CHANNEL_STANDARD_FIELD_IDS.syncStage,
@ -282,7 +282,7 @@ export class MessageChannelWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconHistory',
})
@WorkspaceIsNullable()
syncStageStartedAt: string;
syncStageStartedAt: string | null;
@WorkspaceField({
standardId: MESSAGE_CHANNEL_STANDARD_FIELD_IDS.throttleFailureCount,

View File

@ -83,7 +83,7 @@ export class MessageParticipantWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideFieldKey: 'messageParticipants',
})
@WorkspaceIsNullable()
person: Relation<PersonWorkspaceEntity>;
person: Relation<PersonWorkspaceEntity> | null;
@WorkspaceRelation({
standardId: MESSAGE_PARTICIPANT_STANDARD_FIELD_IDS.workspaceMember,
@ -96,5 +96,5 @@ export class MessageParticipantWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideFieldKey: 'messageParticipants',
})
@WorkspaceIsNullable()
workspaceMember: Relation<WorkspaceMemberWorkspaceEntity>;
workspaceMember: Relation<WorkspaceMemberWorkspaceEntity> | null;
}

View File

@ -78,7 +78,7 @@ export class MessageWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconCalendar',
})
@WorkspaceIsNullable()
receivedAt: string;
receivedAt: string | null;
@WorkspaceRelation({
standardId: MESSAGE_STANDARD_FIELD_IDS.messageThread,
@ -92,7 +92,7 @@ export class MessageWorkspaceEntity extends BaseWorkspaceEntity {
onDelete: RelationOnDeleteAction.CASCADE,
})
@WorkspaceIsNullable()
messageThread: Relation<MessageThreadWorkspaceEntity>;
messageThread: Relation<MessageThreadWorkspaceEntity> | null;
@WorkspaceRelation({
standardId: MESSAGE_STANDARD_FIELD_IDS.messageParticipants,

View File

@ -1,4 +1,4 @@
import { Logger } from '@nestjs/common';
import { Logger, Scope } from '@nestjs/common';
import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator';
@ -21,7 +21,10 @@ export type MessagingMessageListFetchJobData = {
workspaceId: string;
};
@Processor(MessageQueue.messagingQueue)
@Processor({
queueName: MessageQueue.messagingQueue,
scope: Scope.REQUEST,
})
export class MessagingMessageListFetchJob {
private readonly logger = new Logger(MessagingMessageListFetchJob.name);

View File

@ -1,3 +1,5 @@
import { Scope } from '@nestjs/common';
import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
@ -18,7 +20,10 @@ export type MessagingMessagesImportJobData = {
workspaceId: string;
};
@Processor(MessageQueue.messagingQueue)
@Processor({
queueName: MessageQueue.messagingQueue,
scope: Scope.REQUEST,
})
export class MessagingMessagesImportJob {
constructor(
@InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity)

View File

@ -25,7 +25,7 @@ export class MessageParticipantListener {
@OnEvent('messageParticipant.matched')
public async handleMessageParticipantMatched(payload: {
workspaceId: string;
userId: string;
workspaceMemberId: string;
messageParticipants: ObjectRecord<MessageParticipantWorkspaceEntity>[];
}): Promise<void> {
const messageParticipants = payload.messageParticipants ?? [];
@ -60,7 +60,7 @@ export class MessageParticipantListener {
properties: null,
objectName: 'message',
recordId: participant.personId,
workspaceMemberId: payload.userId,
workspaceMemberId: payload.workspaceMemberId,
workspaceId: payload.workspaceId,
linkedObjectMetadataId: messageObjectMetadata.id,
linkedRecordId: participant.messageId,

View File

@ -67,6 +67,9 @@ export class MessagingMessageChannelSyncStatusMonitoringCronJob {
await this.messageChannelRepository.getAll(workspaceId);
for (const messageChannel of messageChannels) {
if (!messageChannel.syncStatus) {
continue;
}
await this.messagingTelemetryService.track({
eventName: `message_channel.monitoring.sync_status.${snakeCase(
messageChannel.syncStatus,

View File

@ -49,7 +49,7 @@ export class OpportunityWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconCurrencyDollar',
})
@WorkspaceIsNullable()
amount: CurrencyMetadata;
amount: CurrencyMetadata | null;
@WorkspaceField({
standardId: OPPORTUNITY_STANDARD_FIELD_IDS.closeDate,
@ -59,7 +59,7 @@ export class OpportunityWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconCalendarEvent',
})
@WorkspaceIsNullable()
closeDate: Date;
closeDate: Date | null;
@WorkspaceField({
standardId: OPPORTUNITY_STANDARD_FIELD_IDS.probability,
@ -102,7 +102,7 @@ export class OpportunityWorkspaceEntity extends BaseWorkspaceEntity {
})
@WorkspaceIsSystem()
@WorkspaceIsNullable()
position: number;
position: number | null;
@WorkspaceRelation({
standardId: OPPORTUNITY_STANDARD_FIELD_IDS.pointOfContact,
@ -116,7 +116,7 @@ export class OpportunityWorkspaceEntity extends BaseWorkspaceEntity {
onDelete: RelationOnDeleteAction.SET_NULL,
})
@WorkspaceIsNullable()
pointOfContact: Relation<PersonWorkspaceEntity>;
pointOfContact: Relation<PersonWorkspaceEntity> | null;
@WorkspaceRelation({
standardId: OPPORTUNITY_STANDARD_FIELD_IDS.company,
@ -130,7 +130,7 @@ export class OpportunityWorkspaceEntity extends BaseWorkspaceEntity {
onDelete: RelationOnDeleteAction.SET_NULL,
})
@WorkspaceIsNullable()
company: Relation<CompanyWorkspaceEntity>;
company: Relation<CompanyWorkspaceEntity> | null;
@WorkspaceRelation({
standardId: OPPORTUNITY_STANDARD_FIELD_IDS.favorites,

View File

@ -41,7 +41,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconUser',
})
@WorkspaceIsNullable()
name: FullNameMetadata;
name: FullNameMetadata | null;
@WorkspaceField({
standardId: PERSON_STANDARD_FIELD_IDS.email,
@ -60,7 +60,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconBrandLinkedin',
})
@WorkspaceIsNullable()
linkedinLink: LinkMetadata;
linkedinLink: LinkMetadata | null;
@WorkspaceField({
standardId: PERSON_STANDARD_FIELD_IDS.xLink,
@ -70,7 +70,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconBrandX',
})
@WorkspaceIsNullable()
xLink: LinkMetadata;
xLink: LinkMetadata | null;
@WorkspaceField({
standardId: PERSON_STANDARD_FIELD_IDS.jobTitle,
@ -118,7 +118,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
})
@WorkspaceIsSystem()
@WorkspaceIsNullable()
position: number;
position: number | null;
// Relations
@WorkspaceRelation({
@ -132,7 +132,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideFieldKey: 'people',
})
@WorkspaceIsNullable()
company: Relation<CompanyWorkspaceEntity>;
company: Relation<CompanyWorkspaceEntity> | null;
@WorkspaceRelation({
standardId: PERSON_STANDARD_FIELD_IDS.pointOfContactForOpportunities,

View File

@ -151,9 +151,9 @@ export class TimelineActivityRepository {
name: string;
properties: Record<string, any> | null;
workspaceMemberId: string | undefined;
recordId: string;
recordId: string | null;
linkedRecordCachedName: string;
linkedRecordId: string | undefined;
linkedRecordId: string | null | undefined;
linkedObjectMetadataId: string | undefined;
}[],
workspaceId: string,

View File

@ -39,7 +39,7 @@ export class AuditLogWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconListDetails',
})
@WorkspaceIsNullable()
properties: JSON;
properties: JSON | null;
@WorkspaceField({
standardId: AUDIT_LOGS_STANDARD_FIELD_IDS.context,
@ -50,7 +50,7 @@ export class AuditLogWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconListDetails',
})
@WorkspaceIsNullable()
context: JSON;
context: JSON | null;
@WorkspaceField({
standardId: AUDIT_LOGS_STANDARD_FIELD_IDS.objectName,
@ -78,7 +78,7 @@ export class AuditLogWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconAbc',
})
@WorkspaceIsNullable()
recordId: string;
recordId: string | null;
@WorkspaceRelation({
standardId: AUDIT_LOGS_STANDARD_FIELD_IDS.workspaceMember,
@ -91,5 +91,5 @@ export class AuditLogWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideFieldKey: 'auditLogs',
})
@WorkspaceIsNullable()
workspaceMember: Relation<WorkspaceMemberWorkspaceEntity>;
workspaceMember: Relation<WorkspaceMemberWorkspaceEntity> | null;
}

View File

@ -56,7 +56,7 @@ export class BehavioralEventWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconListDetails',
})
@WorkspaceIsNullable()
properties: JSON;
properties: JSON | null;
@WorkspaceField({
standardId: BEHAVIORAL_EVENT_STANDARD_FIELD_IDS.context,
@ -67,7 +67,7 @@ export class BehavioralEventWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconListDetails',
})
@WorkspaceIsNullable()
context: JSON;
context: JSON | null;
@WorkspaceField({
standardId: BEHAVIORAL_EVENT_STANDARD_FIELD_IDS.objectName,
@ -86,5 +86,5 @@ export class BehavioralEventWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconAbc',
})
@WorkspaceIsNullable()
recordId: string;
recordId: string | null;
}

View File

@ -56,7 +56,7 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconListDetails',
})
@WorkspaceIsNullable()
properties: JSON;
properties: JSON | null;
// Special objects that don't have their own timeline and are 'link' to the main object
@WorkspaceField({
@ -76,7 +76,7 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconAbc',
})
@WorkspaceIsNullable()
linkedRecordId: string;
linkedRecordId: string | null;
@WorkspaceField({
standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.linkedObjectMetadataId,
@ -86,7 +86,7 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconAbc',
})
@WorkspaceIsNullable()
linkedObjectMetadataId: string;
linkedObjectMetadataId: string | null;
// Who made the action
@WorkspaceRelation({
@ -100,7 +100,7 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideFieldKey: 'timelineActivities',
})
@WorkspaceIsNullable()
workspaceMember: Relation<WorkspaceMemberWorkspaceEntity>;
workspaceMember: Relation<WorkspaceMemberWorkspaceEntity> | null;
@WorkspaceRelation({
standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.person,
@ -113,7 +113,7 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideFieldKey: 'timelineActivities',
})
@WorkspaceIsNullable()
person: Relation<PersonWorkspaceEntity>;
person: Relation<PersonWorkspaceEntity> | null;
@WorkspaceRelation({
standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.company,
@ -126,7 +126,7 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideFieldKey: 'timelineActivities',
})
@WorkspaceIsNullable()
company: Relation<CompanyWorkspaceEntity>;
company: Relation<CompanyWorkspaceEntity> | null;
@WorkspaceRelation({
standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.opportunity,
@ -139,7 +139,7 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideFieldKey: 'timelineActivities',
})
@WorkspaceIsNullable()
opportunity: Relation<OpportunityWorkspaceEntity>;
opportunity: Relation<OpportunityWorkspaceEntity> | null;
@WorkspaceDynamicRelation({
type: RelationMetadataType.MANY_TO_ONE,

View File

@ -72,5 +72,5 @@ export class ViewFieldWorkspaceEntity extends BaseWorkspaceEntity {
joinColumn: 'viewId',
})
@WorkspaceIsNullable()
view?: ViewWorkspaceEntity;
view?: ViewWorkspaceEntity | null;
}

View File

@ -68,5 +68,5 @@ export class ViewFilterWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideFieldKey: 'viewFilters',
})
@WorkspaceIsNullable()
view: Relation<ViewWorkspaceEntity>;
view: Relation<ViewWorkspaceEntity> | null;
}

View File

@ -53,5 +53,5 @@ export class ViewSortWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideFieldKey: 'viewSorts',
})
@WorkspaceIsNullable()
view: Relation<ViewWorkspaceEntity>;
view: Relation<ViewWorkspaceEntity> | null;
}

View File

@ -3,10 +3,9 @@ import { Injectable } from '@nestjs/common';
import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface';
import { DeleteOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { CommentRepository } from 'src/modules/activity/repositories/comment.repository';
import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { CommentWorkspaceEntity } from 'src/modules/activity/standard-objects/comment.workspace-entity';
import { AttachmentRepository } from 'src/modules/attachment/repositories/attachment.repository';
import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
@Injectable()
@ -14,10 +13,10 @@ export class WorkspaceMemberDeleteOnePreQueryHook
implements WorkspacePreQueryHook
{
constructor(
@InjectObjectMetadataRepository(AttachmentWorkspaceEntity)
private readonly attachmentRepository: AttachmentRepository,
@InjectObjectMetadataRepository(CommentWorkspaceEntity)
private readonly commentRepository: CommentRepository,
@InjectWorkspaceRepository(AttachmentWorkspaceEntity)
private readonly attachmentRepository: WorkspaceRepository<AttachmentWorkspaceEntity>,
@InjectWorkspaceRepository(CommentWorkspaceEntity)
private readonly commentRepository: WorkspaceRepository<CommentWorkspaceEntity>,
) {}
// There is no need to validate the user's access to the workspace member since we don't have permission yet.
@ -26,16 +25,18 @@ export class WorkspaceMemberDeleteOnePreQueryHook
workspaceId: string,
payload: DeleteOneResolverArgs,
): Promise<void> {
const workspaceMemberId = payload.id;
const authorId = payload.id;
await this.attachmentRepository.deleteByAuthorId(
workspaceMemberId,
workspaceId,
);
await this.attachmentRepository.delete({
author: {
id: authorId,
},
});
await this.commentRepository.deleteByAuthorId(
workspaceMemberId,
workspaceId,
);
await this.commentRepository.delete({
author: {
id: authorId,
},
});
}
}

View File

@ -1,6 +1,6 @@
import { Module } from '@nestjs/common';
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
import { CommentWorkspaceEntity } from 'src/modules/activity/standard-objects/comment.workspace-entity';
import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
import { WorkspaceMemberDeleteManyPreQueryHook } from 'src/modules/workspace-member/query-hooks/workspace-member-delete-many.pre-query.hook';
@ -8,7 +8,7 @@ import { WorkspaceMemberDeleteOnePreQueryHook } from 'src/modules/workspace-memb
@Module({
imports: [
ObjectMetadataRepositoryModule.forFeature([
TwentyORMModule.forFeature([
AttachmentWorkspaceEntity,
CommentWorkspaceEntity,
]),

View File

@ -0,0 +1,3 @@
export const isDefined = <T>(value: T | null | undefined): value is T => {
return value !== null && value !== undefined;
};

View File

@ -8,6 +8,7 @@ export interface ReflectMetadataTypeMap {
['workspace:is-system-metadata-args']: true;
['workspace:is-audit-logged-metadata-args']: false;
['workspace:is-primary-field-metadata-args']: true;
['workspace:join-column']: true;
}
export class TypedReflect {