Refactor backend folder structure (#4505)
* Refactor backend folder structure Co-authored-by: Charles Bochet <charles@twenty.com> * fix tests * fix * move yoga hooks --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -0,0 +1,41 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { FeatureFlagEntity } from 'src/engine/modules/feature-flag/feature-flag.entity';
|
||||
import { EnvironmentModule } from 'src/engine/integrations/environment/environment.module';
|
||||
import { CreateCompaniesAndContactsModule } from 'src/modules/connected-account/auto-companies-and-contacts-creation/create-company-and-contact/create-company-and-contact.module';
|
||||
import { BlocklistModule } from 'src/modules/connected-account/repositories/blocklist/blocklist.module';
|
||||
import { ConnectedAccountModule } from 'src/modules/connected-account/repositories/connected-account/connected-account.module';
|
||||
import { CalendarChannelEventAssociationModule } from 'src/modules/calendar/repositories/calendar-channel-event-association/calendar-channel-event-assocation.module';
|
||||
import { CalendarChannelModule } from 'src/modules/calendar/repositories/calendar-channel/calendar-channel.module';
|
||||
import { CalendarEventAttendeeModule } from 'src/modules/calendar/repositories/calendar-event-attendee/calendar-event-attendee.module';
|
||||
import { CalendarEventModule } from 'src/modules/calendar/repositories/calendar-event/calendar-event.module';
|
||||
import { CalendarEventCleanerModule } from 'src/modules/calendar/services/calendar-event-cleaner/calendar-event-cleaner.module';
|
||||
import { GoogleCalendarFullSyncService } from 'src/modules/calendar/services/google-calendar-full-sync.service';
|
||||
import { GoogleCalendarClientProvider } from 'src/modules/calendar/services/providers/google-calendar/google-calendar.provider';
|
||||
import { CompanyModule } from 'src/modules/messaging/repositories/company/company.module';
|
||||
import { PersonModule } from 'src/modules/person/repositories/person/person.module';
|
||||
import { WorkspaceMemberModule } from 'src/modules/workspace-member/repositories/workspace-member/workspace-member.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
EnvironmentModule,
|
||||
WorkspaceDataSourceModule,
|
||||
ConnectedAccountModule,
|
||||
CalendarChannelModule,
|
||||
CalendarChannelEventAssociationModule,
|
||||
CalendarEventModule,
|
||||
CalendarEventAttendeeModule,
|
||||
CreateCompaniesAndContactsModule,
|
||||
WorkspaceMemberModule,
|
||||
TypeOrmModule.forFeature([FeatureFlagEntity], 'core'),
|
||||
CompanyModule,
|
||||
PersonModule,
|
||||
BlocklistModule,
|
||||
CalendarEventCleanerModule,
|
||||
],
|
||||
providers: [GoogleCalendarFullSyncService, GoogleCalendarClientProvider],
|
||||
exports: [GoogleCalendarFullSyncService],
|
||||
})
|
||||
export class CalendarModule {}
|
||||
@ -0,0 +1,66 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
|
||||
import { Command, CommandRunner, Option } from 'nest-commander';
|
||||
|
||||
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
|
||||
import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service';
|
||||
import { ConnectedAccountService } from 'src/modules/connected-account/repositories/connected-account/connected-account.service';
|
||||
import {
|
||||
GoogleCalendarFullSyncJobData,
|
||||
GoogleCalendarFullSyncJob,
|
||||
} from 'src/modules/calendar/jobs/google-calendar-full-sync.job';
|
||||
|
||||
interface GoogleCalendarFullSyncOptions {
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
@Command({
|
||||
name: 'workspace:google-calendar-full-sync',
|
||||
description:
|
||||
'Start google calendar full-sync for all workspaceMembers in a workspace.',
|
||||
})
|
||||
export class GoogleCalendarFullSyncCommand extends CommandRunner {
|
||||
constructor(
|
||||
@Inject(MessageQueue.messagingQueue)
|
||||
private readonly messageQueueService: MessageQueueService,
|
||||
private readonly connectedAccountService: ConnectedAccountService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async run(
|
||||
_passedParam: string[],
|
||||
options: GoogleCalendarFullSyncOptions,
|
||||
): Promise<void> {
|
||||
await this.fetchWorkspaceCalendars(options.workspaceId);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '-w, --workspace-id [workspace_id]',
|
||||
description: 'workspace id',
|
||||
required: true,
|
||||
})
|
||||
parseWorkspaceId(value: string): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
private async fetchWorkspaceCalendars(workspaceId: string): Promise<void> {
|
||||
const connectedAccounts =
|
||||
await this.connectedAccountService.getAll(workspaceId);
|
||||
|
||||
for (const connectedAccount of connectedAccounts) {
|
||||
await this.messageQueueService.add<GoogleCalendarFullSyncJobData>(
|
||||
GoogleCalendarFullSyncJob.name,
|
||||
{
|
||||
workspaceId,
|
||||
connectedAccountId: connectedAccount.id,
|
||||
},
|
||||
{
|
||||
retryLimit: 2,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { FeatureFlagEntity } from 'src/engine/modules/feature-flag/feature-flag.entity';
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
import { DataSourceModule } from 'src/engine-metadata/data-source/data-source.module';
|
||||
import { ConnectedAccountModule } from 'src/modules/connected-account/repositories/connected-account/connected-account.module';
|
||||
import { GoogleCalendarFullSyncCommand } from 'src/modules/calendar/commands/google-calendar-full-sync.command';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
DataSourceModule,
|
||||
TypeORMModule,
|
||||
TypeOrmModule.forFeature([FeatureFlagEntity], 'core'),
|
||||
ConnectedAccountModule,
|
||||
],
|
||||
providers: [GoogleCalendarFullSyncCommand],
|
||||
})
|
||||
export class WorkspaceCalendarSyncCommandsModule {}
|
||||
@ -0,0 +1,53 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { MessageQueueJob } from 'src/engine/integrations/message-queue/interfaces/message-queue-job.interface';
|
||||
|
||||
import { GoogleAPIsRefreshAccessTokenService } from 'src/modules/connected-account/services/google-apis-refresh-access-token.service';
|
||||
import { GoogleCalendarFullSyncService } from 'src/modules/calendar/services/google-calendar-full-sync.service';
|
||||
|
||||
export type GoogleCalendarFullSyncJobData = {
|
||||
workspaceId: string;
|
||||
connectedAccountId: string;
|
||||
nextPageToken?: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class GoogleCalendarFullSyncJob
|
||||
implements MessageQueueJob<GoogleCalendarFullSyncJobData>
|
||||
{
|
||||
private readonly logger = new Logger(GoogleCalendarFullSyncJob.name);
|
||||
|
||||
constructor(
|
||||
private readonly googleAPIsRefreshAccessTokenService: GoogleAPIsRefreshAccessTokenService,
|
||||
private readonly googleCalendarFullSyncService: GoogleCalendarFullSyncService,
|
||||
) {}
|
||||
|
||||
async handle(data: GoogleCalendarFullSyncJobData): Promise<void> {
|
||||
this.logger.log(
|
||||
`google calendar full-sync for workspace ${
|
||||
data.workspaceId
|
||||
} and account ${data.connectedAccountId} ${
|
||||
data.nextPageToken ? `and ${data.nextPageToken} pageToken` : ''
|
||||
}`,
|
||||
);
|
||||
try {
|
||||
await this.googleAPIsRefreshAccessTokenService.refreshAndSaveAccessToken(
|
||||
data.workspaceId,
|
||||
data.connectedAccountId,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(
|
||||
`Error refreshing access token for connected account ${data.connectedAccountId} in workspace ${data.workspaceId}`,
|
||||
e,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await this.googleCalendarFullSyncService.startGoogleCalendarFullSync(
|
||||
data.workspaceId,
|
||||
data.connectedAccountId,
|
||||
data.nextPageToken,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { CalendarChannelEventAssociationService } from 'src/modules/calendar/repositories/calendar-channel-event-association/calendar-channel-event-association.service';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
|
||||
@Module({
|
||||
imports: [WorkspaceDataSourceModule],
|
||||
providers: [CalendarChannelEventAssociationService],
|
||||
exports: [CalendarChannelEventAssociationService],
|
||||
})
|
||||
export class CalendarChannelEventAssociationModule {}
|
||||
@ -0,0 +1,172 @@
|
||||
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 { CalendarChannelEventAssociationObjectMetadata } from 'src/modules/calendar/standard-objects/calendar-channel-event-association.object-metadata';
|
||||
import { getFlattenedValuesAndValuesStringForBatchRawQuery } from 'src/modules/calendar/utils/getFlattenedValuesAndValuesStringForBatchRawQuery.util';
|
||||
|
||||
@Injectable()
|
||||
export class CalendarChannelEventAssociationService {
|
||||
constructor(
|
||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||
) {}
|
||||
|
||||
public async getByEventExternalIdsAndCalendarChannelId(
|
||||
eventExternalIds: string[],
|
||||
calendarChannelId: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<ObjectRecord<CalendarChannelEventAssociationObjectMetadata>[]> {
|
||||
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<CalendarChannelEventAssociationObjectMetadata>[]> {
|
||||
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<CalendarChannelEventAssociationObjectMetadata>[]> {
|
||||
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<CalendarChannelEventAssociationObjectMetadata>,
|
||||
'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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { CalendarChannelService } from 'src/modules/calendar/repositories/calendar-channel/calendar-channel.service';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
|
||||
@Module({
|
||||
imports: [WorkspaceDataSourceModule],
|
||||
providers: [CalendarChannelService],
|
||||
exports: [CalendarChannelService],
|
||||
})
|
||||
export class CalendarChannelModule {}
|
||||
@ -0,0 +1,76 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { EntityManager } from 'typeorm';
|
||||
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
import { CalendarChannelObjectMetadata } from 'src/modules/calendar/standard-objects/calendar-channel.object-metadata';
|
||||
import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record';
|
||||
|
||||
@Injectable()
|
||||
export class CalendarChannelService {
|
||||
constructor(
|
||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||
) {}
|
||||
|
||||
public async getByConnectedAccountId(
|
||||
connectedAccountId: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<ObjectRecord<CalendarChannelObjectMetadata>[]> {
|
||||
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 getFirstByConnectedAccountIdOrFail(
|
||||
connectedAccountId: string,
|
||||
workspaceId: string,
|
||||
): Promise<ObjectRecord<CalendarChannelObjectMetadata>> {
|
||||
const calendarChannels = await this.getByConnectedAccountId(
|
||||
connectedAccountId,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (!calendarChannels || calendarChannels.length === 0) {
|
||||
throw new Error(
|
||||
`No calendar channel found for connected account ${connectedAccountId} in workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
return calendarChannels[0];
|
||||
}
|
||||
|
||||
public async getIsContactAutoCreationEnabledByConnectedAccountIdOrFail(
|
||||
connectedAccountId: string,
|
||||
workspaceId: string,
|
||||
): Promise<boolean> {
|
||||
const calendarChannel = await this.getFirstByConnectedAccountIdOrFail(
|
||||
connectedAccountId,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
return calendarChannel.isContactAutoCreationEnabled;
|
||||
}
|
||||
|
||||
public async getByIds(
|
||||
ids: string[],
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<ObjectRecord<CalendarChannelObjectMetadata>[]> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
return await this.workspaceDataSourceService.executeRawQuery(
|
||||
`SELECT * FROM ${dataSourceSchema}."calendarChannel" WHERE "id" = ANY($1)`,
|
||||
[ids],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { CalendarEventAttendeeService } from 'src/modules/calendar/repositories/calendar-event-attendee/calendar-event-attendee.service';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
|
||||
@Module({
|
||||
imports: [WorkspaceDataSourceModule],
|
||||
providers: [CalendarEventAttendeeService],
|
||||
exports: [CalendarEventAttendeeService],
|
||||
})
|
||||
export class CalendarEventAttendeeModule {}
|
||||
@ -0,0 +1,151 @@
|
||||
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 { CalendarEventAttendeeObjectMetadata } from 'src/modules/calendar/standard-objects/calendar-event-attendee.object-metadata';
|
||||
import { getFlattenedValuesAndValuesStringForBatchRawQuery } from 'src/modules/calendar/utils/getFlattenedValuesAndValuesStringForBatchRawQuery.util';
|
||||
import { CalendarEventAttendee } from 'src/modules/calendar/types/calendar-event';
|
||||
|
||||
@Injectable()
|
||||
export class CalendarEventAttendeeService {
|
||||
constructor(
|
||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||
) {}
|
||||
|
||||
public async getByIds(
|
||||
calendarEventAttendeeIds: string[],
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<ObjectRecord<CalendarEventAttendeeObjectMetadata>[]> {
|
||||
if (calendarEventAttendeeIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
return await this.workspaceDataSourceService.executeRawQuery(
|
||||
`SELECT * FROM ${dataSourceSchema}."calendarEventAttendees" WHERE "id" = ANY($1)`,
|
||||
[calendarEventAttendeeIds],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
public async getByCalendarEventIds(
|
||||
calendarEventIds: string[],
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<ObjectRecord<CalendarEventAttendeeObjectMetadata>[]> {
|
||||
if (calendarEventIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
return await this.workspaceDataSourceService.executeRawQuery(
|
||||
`SELECT * FROM ${dataSourceSchema}."calendarEventAttendees" WHERE "calendarEventId" = ANY($1)`,
|
||||
[calendarEventIds],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
public async deleteByIds(
|
||||
calendarEventAttendeeIds: string[],
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<void> {
|
||||
if (calendarEventAttendeeIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
await this.workspaceDataSourceService.executeRawQuery(
|
||||
`DELETE FROM ${dataSourceSchema}."calendarEventAttendees" WHERE "id" = ANY($1)`,
|
||||
[calendarEventAttendeeIds],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
public async saveCalendarEventAttendees(
|
||||
calendarEventAttendees: CalendarEventAttendee[],
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<void> {
|
||||
if (calendarEventAttendees.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
const { flattenedValues, valuesString } =
|
||||
getFlattenedValuesAndValuesStringForBatchRawQuery(
|
||||
calendarEventAttendees,
|
||||
{
|
||||
calendarEventId: 'uuid',
|
||||
handle: 'text',
|
||||
displayName: 'text',
|
||||
isOrganizer: 'boolean',
|
||||
responseStatus: `${dataSourceSchema}."calendarEventAttendee_responsestatus_enum"`,
|
||||
},
|
||||
);
|
||||
|
||||
await this.workspaceDataSourceService.executeRawQuery(
|
||||
`INSERT INTO ${dataSourceSchema}."calendarEventAttendee" ("calendarEventId", "handle", "displayName", "isOrganizer", "responseStatus") VALUES ${valuesString}`,
|
||||
flattenedValues,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
public async updateCalendarEventAttendees(
|
||||
calendarEventAttendees: CalendarEventAttendee[],
|
||||
iCalUIDCalendarEventIdMap: Map<string, string>,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<void> {
|
||||
if (calendarEventAttendees.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
const values = calendarEventAttendees.map((calendarEventAttendee) => ({
|
||||
...calendarEventAttendee,
|
||||
calendarEventId: iCalUIDCalendarEventIdMap.get(
|
||||
calendarEventAttendee.iCalUID,
|
||||
),
|
||||
}));
|
||||
|
||||
const { flattenedValues, valuesString } =
|
||||
getFlattenedValuesAndValuesStringForBatchRawQuery(values, {
|
||||
calendarEventId: 'uuid',
|
||||
handle: 'text',
|
||||
displayName: 'text',
|
||||
isOrganizer: 'boolean',
|
||||
responseStatus: `${dataSourceSchema}."calendarEventAttendee_responsestatus_enum"`,
|
||||
});
|
||||
|
||||
await this.workspaceDataSourceService.executeRawQuery(
|
||||
`UPDATE ${dataSourceSchema}."calendarEventAttendee" AS "calendarEventAttendee"
|
||||
SET "displayName" = "newValues"."displayName",
|
||||
"isOrganizer" = "newValues"."isOrganizer",
|
||||
"responseStatus" = "newValues"."responseStatus"
|
||||
FROM (VALUES ${valuesString}) AS "newValues"("calendarEventId", "handle", "displayName", "isOrganizer", "responseStatus")
|
||||
WHERE "calendarEventAttendee"."handle" = "newValues"."handle"
|
||||
AND "calendarEventAttendee"."calendarEventId" = "newValues"."calendarEventId"`,
|
||||
flattenedValues,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { CalendarEventService } from 'src/modules/calendar/repositories/calendar-event/calendar-event.service';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
|
||||
@Module({
|
||||
imports: [WorkspaceDataSourceModule],
|
||||
providers: [CalendarEventService],
|
||||
exports: [CalendarEventService],
|
||||
})
|
||||
export class CalendarEventModule {}
|
||||
@ -0,0 +1,202 @@
|
||||
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 { CalendarEventObjectMetadata } from 'src/modules/calendar/standard-objects/calendar-event.object-metadata';
|
||||
import { getFlattenedValuesAndValuesStringForBatchRawQuery } from 'src/modules/calendar/utils/getFlattenedValuesAndValuesStringForBatchRawQuery.util';
|
||||
import { CalendarEvent } from 'src/modules/calendar/types/calendar-event';
|
||||
import { CalendarEventAttendeeObjectMetadata } from 'src/modules/calendar/standard-objects/calendar-event-attendee.object-metadata';
|
||||
|
||||
@Injectable()
|
||||
export class CalendarEventService {
|
||||
constructor(
|
||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||
) {}
|
||||
|
||||
public async getByIds(
|
||||
calendarEventIds: string[],
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<ObjectRecord<CalendarEventObjectMetadata>[]> {
|
||||
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 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<CalendarEventAttendeeObjectMetadata>[]> {
|
||||
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;
|
||||
}[] = await this.workspaceDataSourceService.executeRawQuery(
|
||||
`SELECT id, "iCalUID" FROM ${dataSourceSchema}."calendarEvent" WHERE "iCalUID" = ANY($1)`,
|
||||
[iCalUIDs],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
const iCalUIDsCalendarEvnetIdsMap = new Map<string, string>();
|
||||
|
||||
calendarEvents.forEach((calendarEvent) => {
|
||||
iCalUIDsCalendarEvnetIdsMap.set(calendarEvent.iCalUID, calendarEvent.id);
|
||||
});
|
||||
|
||||
return iCalUIDsCalendarEvnetIdsMap;
|
||||
}
|
||||
|
||||
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',
|
||||
conferenceUri: 'text',
|
||||
recurringEventExternalId: 'text',
|
||||
});
|
||||
|
||||
await this.workspaceDataSourceService.executeRawQuery(
|
||||
`INSERT INTO ${dataSourceSchema}."calendarEvent" ("id", "title", "isCanceled", "isFullDay", "startsAt", "endsAt", "externalCreatedAt", "externalUpdatedAt", "description", "location", "iCalUID", "conferenceSolution", "conferenceUri", "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',
|
||||
conferenceUri: '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",
|
||||
"conferenceUri" = "newData"."conferenceUri",
|
||||
"recurringEventExternalId" = "newData"."recurringEventExternalId"
|
||||
FROM (VALUES ${valuesString})
|
||||
AS "newData"("title", "isCanceled", "isFullDay", "startsAt", "endsAt", "externalCreatedAt", "externalUpdatedAt", "description", "location", "iCalUID", "conferenceSolution", "conferenceUri", "recurringEventExternalId")
|
||||
WHERE "calendarEvent"."iCalUID" = "newData"."iCalUID"`,
|
||||
flattenedValues,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
import { DataSourceModule } from 'src/engine-metadata/data-source/data-source.module';
|
||||
import { CalendarEventModule } from 'src/modules/calendar/repositories/calendar-event/calendar-event.module';
|
||||
import { CalendarEventCleanerService } from 'src/modules/calendar/services/calendar-event-cleaner/calendar-event-cleaner.service';
|
||||
|
||||
@Module({
|
||||
imports: [DataSourceModule, TypeORMModule, CalendarEventModule],
|
||||
providers: [CalendarEventCleanerService],
|
||||
exports: [CalendarEventCleanerService],
|
||||
})
|
||||
export class CalendarEventCleanerModule {}
|
||||
@ -0,0 +1,20 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { CalendarEventService } from 'src/modules/calendar/repositories/calendar-event/calendar-event.service';
|
||||
import { deleteUsingPagination } from 'src/modules/messaging/services/thread-cleaner/utils/delete-using-pagination.util';
|
||||
|
||||
@Injectable()
|
||||
export class CalendarEventCleanerService {
|
||||
constructor(private readonly calendarEventService: CalendarEventService) {}
|
||||
|
||||
public async cleanWorkspaceCalendarEvents(workspaceId: string) {
|
||||
await deleteUsingPagination(
|
||||
workspaceId,
|
||||
500,
|
||||
this.calendarEventService.getNonAssociatedCalendarEventIdsPaginated.bind(
|
||||
this.calendarEventService,
|
||||
),
|
||||
this.calendarEventService.deleteByIds.bind(this.calendarEventService),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,274 @@
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ConnectedAccountService } from 'src/modules/connected-account/repositories/connected-account/connected-account.service';
|
||||
import { BlocklistService } from 'src/modules/connected-account/repositories/blocklist/blocklist.service';
|
||||
import {
|
||||
FeatureFlagEntity,
|
||||
FeatureFlagKeys,
|
||||
} from 'src/engine/modules/feature-flag/feature-flag.entity';
|
||||
import { GoogleCalendarClientProvider } from 'src/modules/calendar/services/providers/google-calendar/google-calendar.provider';
|
||||
import { googleCalendarSearchFilterExcludeEmails } from 'src/modules/calendar/utils/google-calendar-search-filter.util';
|
||||
import { CalendarChannelEventAssociationService } from 'src/modules/calendar/repositories/calendar-channel-event-association/calendar-channel-event-association.service';
|
||||
import { CalendarChannelService } from 'src/modules/calendar/repositories/calendar-channel/calendar-channel.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 { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
import { CalendarEventService } from 'src/modules/calendar/repositories/calendar-event/calendar-event.service';
|
||||
import { formatGoogleCalendarEvent } from 'src/modules/calendar/utils/format-google-calendar-event.util';
|
||||
import { GoogleCalendarFullSyncJobData } from 'src/modules/calendar/jobs/google-calendar-full-sync.job';
|
||||
import { CalendarEventAttendeeService } from 'src/modules/calendar/repositories/calendar-event-attendee/calendar-event-attendee.service';
|
||||
|
||||
@Injectable()
|
||||
export class GoogleCalendarFullSyncService {
|
||||
private readonly logger = new Logger(GoogleCalendarFullSyncService.name);
|
||||
|
||||
constructor(
|
||||
private readonly googleCalendarClientProvider: GoogleCalendarClientProvider,
|
||||
@Inject(MessageQueue.calendarQueue)
|
||||
private readonly messageQueueService: MessageQueueService,
|
||||
private readonly connectedAccountService: ConnectedAccountService,
|
||||
private readonly calendarEventService: CalendarEventService,
|
||||
private readonly calendarChannelService: CalendarChannelService,
|
||||
private readonly calendarChannelEventAssociationService: CalendarChannelEventAssociationService,
|
||||
private readonly calendarEventAttendeesService: CalendarEventAttendeeService,
|
||||
private readonly blocklistService: BlocklistService,
|
||||
@InjectRepository(FeatureFlagEntity, 'core')
|
||||
private readonly featureFlagRepository: Repository<FeatureFlagEntity>,
|
||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||
) {}
|
||||
|
||||
public async startGoogleCalendarFullSync(
|
||||
workspaceId: string,
|
||||
connectedAccountId: string,
|
||||
pageToken?: string,
|
||||
): Promise<void> {
|
||||
const connectedAccount = await this.connectedAccountService.getById(
|
||||
connectedAccountId,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (!connectedAccount) {
|
||||
return;
|
||||
}
|
||||
|
||||
const refreshToken = connectedAccount.refreshToken;
|
||||
const workspaceMemberId = connectedAccount.accountOwnerId;
|
||||
|
||||
if (!refreshToken) {
|
||||
throw new Error(
|
||||
`No refresh token found for connected account ${connectedAccountId} in workspace ${workspaceId} during full-sync`,
|
||||
);
|
||||
}
|
||||
|
||||
const calendarChannel =
|
||||
await this.calendarChannelService.getFirstByConnectedAccountIdOrFail(
|
||||
connectedAccountId,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const calendarChannelId = calendarChannel.id;
|
||||
|
||||
const googleCalendarClient =
|
||||
await this.googleCalendarClientProvider.getGoogleCalendarClient(
|
||||
refreshToken,
|
||||
);
|
||||
|
||||
const isBlocklistEnabledFeatureFlag =
|
||||
await this.featureFlagRepository.findOneBy({
|
||||
workspaceId,
|
||||
key: FeatureFlagKeys.IsBlocklistEnabled,
|
||||
value: true,
|
||||
});
|
||||
|
||||
const isBlocklistEnabled =
|
||||
isBlocklistEnabledFeatureFlag && isBlocklistEnabledFeatureFlag.value;
|
||||
|
||||
const blocklist = isBlocklistEnabled
|
||||
? await this.blocklistService.getByWorkspaceMemberId(
|
||||
workspaceMemberId,
|
||||
workspaceId,
|
||||
)
|
||||
: [];
|
||||
|
||||
const blocklistedEmails = blocklist.map((blocklist) => blocklist.handle);
|
||||
let startTime = Date.now();
|
||||
|
||||
const googleCalendarEvents = await googleCalendarClient.events.list({
|
||||
calendarId: 'primary',
|
||||
maxResults: 500,
|
||||
pageToken: pageToken,
|
||||
q: googleCalendarSearchFilterExcludeEmails(blocklistedEmails),
|
||||
});
|
||||
|
||||
let endTime = Date.now();
|
||||
|
||||
this.logger.log(
|
||||
`google calendar full-sync for workspace ${workspaceId} and account ${connectedAccountId} getting events list in ${
|
||||
endTime - startTime
|
||||
}ms.`,
|
||||
);
|
||||
|
||||
const {
|
||||
items: events,
|
||||
nextPageToken,
|
||||
nextSyncToken,
|
||||
} = googleCalendarEvents.data;
|
||||
|
||||
if (!events || events?.length === 0) {
|
||||
this.logger.log(
|
||||
`google calendar full-sync for workspace ${workspaceId} and account ${connectedAccountId} done with nothing to import.`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const eventExternalIds = events.map((event) => event.id as string);
|
||||
|
||||
startTime = Date.now();
|
||||
|
||||
const existingCalendarChannelEventAssociations =
|
||||
await this.calendarChannelEventAssociationService.getByEventExternalIdsAndCalendarChannelId(
|
||||
eventExternalIds,
|
||||
calendarChannelId,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
endTime = Date.now();
|
||||
|
||||
this.logger.log(
|
||||
`google calendar full-sync for workspace ${workspaceId} and account ${connectedAccountId}: getting existing calendar channel event associations in ${
|
||||
endTime - startTime
|
||||
}ms.`,
|
||||
);
|
||||
|
||||
// TODO: In V2, we will also import deleted events by doing batch GET queries on the canceled events
|
||||
// The canceled events start and end are not accessible in the list query
|
||||
const formattedEvents = events
|
||||
.filter((event) => event.status !== 'cancelled')
|
||||
.map((event) => formatGoogleCalendarEvent(event));
|
||||
|
||||
// TODO: When we will be able to add unicity contraint on iCalUID, we will do a INSERT ON CONFLICT DO UPDATE
|
||||
|
||||
const existingEventExternalIds =
|
||||
existingCalendarChannelEventAssociations.map(
|
||||
(association) => association.eventExternalId,
|
||||
);
|
||||
|
||||
const eventsToSave = formattedEvents.filter(
|
||||
(event) => !existingEventExternalIds.includes(event.externalId),
|
||||
);
|
||||
|
||||
const eventsToUpdate = formattedEvents.filter((event) =>
|
||||
existingEventExternalIds.includes(event.externalId),
|
||||
);
|
||||
|
||||
const calendarChannelEventAssociationsToSave = eventsToSave.map(
|
||||
(event) => ({
|
||||
calendarEventId: event.id,
|
||||
eventExternalId: event.externalId,
|
||||
calendarChannelId,
|
||||
}),
|
||||
);
|
||||
|
||||
const attendeesToSave = eventsToSave.flatMap((event) => event.attendees);
|
||||
|
||||
const attendeesToUpdate = eventsToUpdate.flatMap(
|
||||
(event) => event.attendees,
|
||||
);
|
||||
|
||||
const iCalUIDCalendarEventIdMap =
|
||||
await this.calendarEventService.getICalUIDCalendarEventIdMap(
|
||||
eventsToUpdate.map((event) => event.iCalUID),
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (events.length > 0) {
|
||||
const dataSourceMetadata =
|
||||
await this.workspaceDataSourceService.connectToWorkspaceDataSource(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
dataSourceMetadata?.transaction(async (transactionManager) => {
|
||||
this.calendarEventService.saveCalendarEvents(
|
||||
eventsToSave,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
this.calendarEventService.updateCalendarEvents(
|
||||
eventsToUpdate,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
this.calendarChannelEventAssociationService.saveCalendarChannelEventAssociations(
|
||||
calendarChannelEventAssociationsToSave,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
this.calendarEventAttendeesService.saveCalendarEventAttendees(
|
||||
attendeesToSave,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
this.calendarEventAttendeesService.updateCalendarEventAttendees(
|
||||
attendeesToUpdate,
|
||||
iCalUIDCalendarEventIdMap,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
this.logger.log(
|
||||
`google calendar full-sync for workspace ${workspaceId} and account ${connectedAccountId} done with nothing to import.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!nextSyncToken) {
|
||||
throw new Error(
|
||||
`No next sync token found for connected account ${connectedAccountId} in workspace ${workspaceId} during full-sync`,
|
||||
);
|
||||
}
|
||||
|
||||
startTime = Date.now();
|
||||
|
||||
// await this.calendarChannelService.updateSyncCursor(
|
||||
// nextSyncToken,
|
||||
// connectedAccount.id,
|
||||
// workspaceId,
|
||||
// );
|
||||
|
||||
endTime = Date.now();
|
||||
|
||||
this.logger.log(
|
||||
`google calendar full-sync for workspace ${workspaceId} and account ${connectedAccountId}: updating sync cursor in ${
|
||||
endTime - startTime
|
||||
}ms.`,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`google calendar full-sync for workspace ${workspaceId} and account ${connectedAccountId} ${
|
||||
nextPageToken ? `and ${nextPageToken} pageToken` : ''
|
||||
} done.`,
|
||||
);
|
||||
|
||||
if (nextPageToken) {
|
||||
await this.messageQueueService.add<GoogleCalendarFullSyncJobData>(
|
||||
GoogleCalendarFullSyncService.name,
|
||||
{
|
||||
workspaceId,
|
||||
connectedAccountId,
|
||||
nextPageToken,
|
||||
},
|
||||
{
|
||||
retryLimit: 2,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { EnvironmentModule } from 'src/engine/integrations/environment/environment.module';
|
||||
import { GoogleCalendarClientProvider } from 'src/modules/calendar/services/providers/google-calendar/google-calendar.provider';
|
||||
|
||||
@Module({
|
||||
imports: [EnvironmentModule],
|
||||
providers: [GoogleCalendarClientProvider],
|
||||
exports: [GoogleCalendarClientProvider],
|
||||
})
|
||||
export class CalendarProvidersModule {}
|
||||
@ -0,0 +1,44 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { OAuth2Client } from 'google-auth-library';
|
||||
import { calendar_v3 as calendarV3, google } from 'googleapis';
|
||||
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
|
||||
@Injectable()
|
||||
export class GoogleCalendarClientProvider {
|
||||
constructor(private readonly environmentService: EnvironmentService) {}
|
||||
|
||||
public async getGoogleCalendarClient(
|
||||
refreshToken: string,
|
||||
): Promise<calendarV3.Calendar> {
|
||||
const oAuth2Client = await this.getOAuth2Client(refreshToken);
|
||||
|
||||
const googleCalendarClient = google.calendar({
|
||||
version: 'v3',
|
||||
auth: oAuth2Client,
|
||||
});
|
||||
|
||||
return googleCalendarClient;
|
||||
}
|
||||
|
||||
private async getOAuth2Client(refreshToken: string): Promise<OAuth2Client> {
|
||||
const googleCalendarClientId = this.environmentService.get(
|
||||
'AUTH_GOOGLE_CLIENT_ID',
|
||||
);
|
||||
const googleCalendarClientSecret = this.environmentService.get(
|
||||
'AUTH_GOOGLE_CLIENT_SECRET',
|
||||
);
|
||||
|
||||
const oAuth2Client = new google.auth.OAuth2(
|
||||
googleCalendarClientId,
|
||||
googleCalendarClientSecret,
|
||||
);
|
||||
|
||||
oAuth2Client.setCredentials({
|
||||
refresh_token: refreshToken,
|
||||
});
|
||||
|
||||
return oAuth2Client;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
import { FeatureFlagKeys } from 'src/engine/modules/feature-flag/feature-flag.entity';
|
||||
import { FieldMetadataType } from 'src/engine-metadata/field-metadata/field-metadata.entity';
|
||||
import { calendarChannelEventAssociationStandardFieldIds } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import { standardObjectIds } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||
import { FieldMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/field-metadata.decorator';
|
||||
import { Gate } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/gate.decorator';
|
||||
import { IsSystem } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/is-system.decorator';
|
||||
import { ObjectMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/object-metadata.decorator';
|
||||
import { BaseObjectMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects/base.object-metadata';
|
||||
import { CalendarEventObjectMetadata } from 'src/modules/calendar/standard-objects/calendar-event.object-metadata';
|
||||
|
||||
@ObjectMetadata({
|
||||
standardId: standardObjectIds.calendarChannelEventAssociation,
|
||||
namePlural: 'calendarChannelEventAssociations',
|
||||
labelSingular: 'Calendar Channel Event Association',
|
||||
labelPlural: 'Calendar Channel Event Associations',
|
||||
description: 'Calendar Channel Event Associations',
|
||||
icon: 'IconCalendar',
|
||||
})
|
||||
@IsSystem()
|
||||
@Gate({
|
||||
featureFlag: FeatureFlagKeys.IsCalendarEnabled,
|
||||
})
|
||||
export class CalendarChannelEventAssociationObjectMetadata extends BaseObjectMetadata {
|
||||
@FieldMetadata({
|
||||
standardId: calendarChannelEventAssociationStandardFieldIds.calendarChannel,
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Channel ID',
|
||||
description: 'Channel ID',
|
||||
icon: 'IconCalendar',
|
||||
joinColumn: 'calendarChannelId',
|
||||
})
|
||||
calendarChannel: CalendarEventObjectMetadata;
|
||||
|
||||
@FieldMetadata({
|
||||
standardId: calendarChannelEventAssociationStandardFieldIds.calendarEvent,
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Event ID',
|
||||
description: 'Event ID',
|
||||
icon: 'IconCalendar',
|
||||
joinColumn: 'calendarEventId',
|
||||
})
|
||||
calendarEvent: CalendarEventObjectMetadata;
|
||||
|
||||
@FieldMetadata({
|
||||
standardId: calendarChannelEventAssociationStandardFieldIds.eventExternalId,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Event external ID',
|
||||
description: 'Event external ID',
|
||||
icon: 'IconCalendar',
|
||||
})
|
||||
eventExternalId: string;
|
||||
}
|
||||
@ -0,0 +1,123 @@
|
||||
import {
|
||||
RelationMetadataType,
|
||||
RelationOnDeleteAction,
|
||||
} from 'src/engine-metadata/relation-metadata/relation-metadata.entity';
|
||||
import { FeatureFlagKeys } from 'src/engine/modules/feature-flag/feature-flag.entity';
|
||||
import { FieldMetadataType } from 'src/engine-metadata/field-metadata/field-metadata.entity';
|
||||
import { calendarChannelStandardFieldIds } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import { standardObjectIds } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||
import { FieldMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/field-metadata.decorator';
|
||||
import { Gate } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/gate.decorator';
|
||||
import { IsSystem } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/is-system.decorator';
|
||||
import { ObjectMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/object-metadata.decorator';
|
||||
import { BaseObjectMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects/base.object-metadata';
|
||||
import { ConnectedAccountObjectMetadata } from 'src/modules/connected-account/standard-objects/connected-account.object-metadata';
|
||||
import { CalendarChannelEventAssociationObjectMetadata } from 'src/modules/calendar/standard-objects/calendar-channel-event-association.object-metadata';
|
||||
import { RelationMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/relation-metadata.decorator';
|
||||
|
||||
export enum CalendarChannelVisibility {
|
||||
METADATA = 'METADATA',
|
||||
SHARE_EVERYTHING = 'SHARE_EVERYTHING',
|
||||
}
|
||||
|
||||
@ObjectMetadata({
|
||||
standardId: standardObjectIds.calendarChannel,
|
||||
namePlural: 'calendarChannels',
|
||||
labelSingular: 'Calendar Channel',
|
||||
labelPlural: 'Calendar Channels',
|
||||
description: 'Calendar Channels',
|
||||
icon: 'IconCalendar',
|
||||
})
|
||||
@IsSystem()
|
||||
@Gate({
|
||||
featureFlag: FeatureFlagKeys.IsCalendarEnabled,
|
||||
})
|
||||
export class CalendarChannelObjectMetadata extends BaseObjectMetadata {
|
||||
@FieldMetadata({
|
||||
standardId: calendarChannelStandardFieldIds.connectedAccount,
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Connected Account',
|
||||
description: 'Connected Account',
|
||||
icon: 'IconUserCircle',
|
||||
joinColumn: 'connectedAccountId',
|
||||
})
|
||||
connectedAccount: ConnectedAccountObjectMetadata;
|
||||
|
||||
@FieldMetadata({
|
||||
standardId: calendarChannelStandardFieldIds.handle,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Handle',
|
||||
description: 'Handle',
|
||||
icon: 'IconAt',
|
||||
})
|
||||
handle: string;
|
||||
|
||||
@FieldMetadata({
|
||||
standardId: calendarChannelStandardFieldIds.visibility,
|
||||
type: FieldMetadataType.SELECT,
|
||||
label: 'Visibility',
|
||||
description: 'Visibility',
|
||||
icon: 'IconEyeglass',
|
||||
options: [
|
||||
{
|
||||
value: CalendarChannelVisibility.METADATA,
|
||||
label: 'Metadata',
|
||||
position: 0,
|
||||
color: 'green',
|
||||
},
|
||||
{
|
||||
value: CalendarChannelVisibility.SHARE_EVERYTHING,
|
||||
label: 'Share Everything',
|
||||
position: 1,
|
||||
color: 'orange',
|
||||
},
|
||||
],
|
||||
defaultValue: { value: CalendarChannelVisibility.SHARE_EVERYTHING },
|
||||
})
|
||||
visibility: string;
|
||||
|
||||
@FieldMetadata({
|
||||
standardId: calendarChannelStandardFieldIds.isContactAutoCreationEnabled,
|
||||
type: FieldMetadataType.BOOLEAN,
|
||||
label: 'Is Contact Auto Creation Enabled',
|
||||
description: 'Is Contact Auto Creation Enabled',
|
||||
icon: 'IconUserCircle',
|
||||
defaultValue: { value: true },
|
||||
})
|
||||
isContactAutoCreationEnabled: boolean;
|
||||
|
||||
@FieldMetadata({
|
||||
standardId: calendarChannelStandardFieldIds.isSyncEnabled,
|
||||
type: FieldMetadataType.BOOLEAN,
|
||||
label: 'Is Sync Enabled',
|
||||
description: 'Is Sync Enabled',
|
||||
icon: 'IconRefresh',
|
||||
defaultValue: { value: true },
|
||||
})
|
||||
isSyncEnabled: boolean;
|
||||
|
||||
@FieldMetadata({
|
||||
standardId: calendarChannelStandardFieldIds.syncCursor,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Sync Cursor',
|
||||
description:
|
||||
'Sync Cursor. Used for syncing events from the calendar provider',
|
||||
icon: 'IconReload',
|
||||
})
|
||||
syncCursor: string;
|
||||
|
||||
@FieldMetadata({
|
||||
standardId:
|
||||
calendarChannelStandardFieldIds.calendarChannelEventAssociations,
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Calendar Channel Event Associations',
|
||||
description: 'Calendar Channel Event Associations',
|
||||
icon: 'IconCalendar',
|
||||
})
|
||||
@RelationMetadata({
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
inverseSideTarget: () => CalendarChannelEventAssociationObjectMetadata,
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
})
|
||||
calendarChannelEventAssociations: CalendarChannelEventAssociationObjectMetadata[];
|
||||
}
|
||||
@ -0,0 +1,128 @@
|
||||
import { FieldMetadataType } from 'src/engine-metadata/field-metadata/field-metadata.entity';
|
||||
import { calendarEventAttendeeStandardFieldIds } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import { standardObjectIds } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||
import { FieldMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/field-metadata.decorator';
|
||||
import { Gate } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/gate.decorator';
|
||||
import { IsNullable } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/is-nullable.decorator';
|
||||
import { IsSystem } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/is-system.decorator';
|
||||
import { ObjectMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/object-metadata.decorator';
|
||||
import { BaseObjectMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects/base.object-metadata';
|
||||
import { CalendarEventObjectMetadata } from 'src/modules/calendar/standard-objects/calendar-event.object-metadata';
|
||||
import { PersonObjectMetadata } from 'src/modules/person/standard-objects/person.object-metadata';
|
||||
import { WorkspaceMemberObjectMetadata } from 'src/modules/workspace-member/standard-objects/workspace-member.object-metadata';
|
||||
|
||||
export enum CalendarEventAttendeeResponseStatus {
|
||||
NEEDS_ACTION = 'NEEDS_ACTION',
|
||||
DECLINED = 'DECLINED',
|
||||
TENTATIVE = 'TENTATIVE',
|
||||
ACCEPTED = 'ACCEPTED',
|
||||
}
|
||||
|
||||
@ObjectMetadata({
|
||||
standardId: standardObjectIds.calendarEventAttendee,
|
||||
namePlural: 'calendarEventAttendees',
|
||||
labelSingular: 'Calendar event attendee',
|
||||
labelPlural: 'Calendar event attendees',
|
||||
description: 'Calendar event attendees',
|
||||
icon: 'IconCalendar',
|
||||
})
|
||||
@IsSystem()
|
||||
@Gate({
|
||||
featureFlag: 'IS_CALENDAR_ENABLED',
|
||||
})
|
||||
export class CalendarEventAttendeeObjectMetadata extends BaseObjectMetadata {
|
||||
@FieldMetadata({
|
||||
standardId: calendarEventAttendeeStandardFieldIds.calendarEvent,
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Event ID',
|
||||
description: 'Event ID',
|
||||
icon: 'IconCalendar',
|
||||
joinColumn: 'calendarEventId',
|
||||
})
|
||||
calendarEvent: CalendarEventObjectMetadata;
|
||||
|
||||
@FieldMetadata({
|
||||
standardId: calendarEventAttendeeStandardFieldIds.handle,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Handle',
|
||||
description: 'Handle',
|
||||
icon: 'IconMail',
|
||||
})
|
||||
handle: string;
|
||||
|
||||
@FieldMetadata({
|
||||
standardId: calendarEventAttendeeStandardFieldIds.displayName,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Display Name',
|
||||
description: 'Display Name',
|
||||
icon: 'IconUser',
|
||||
})
|
||||
displayName: string;
|
||||
|
||||
@FieldMetadata({
|
||||
standardId: calendarEventAttendeeStandardFieldIds.isOrganizer,
|
||||
type: FieldMetadataType.BOOLEAN,
|
||||
label: 'Is Organizer',
|
||||
description: 'Is Organizer',
|
||||
icon: 'IconUser',
|
||||
})
|
||||
isOrganizer: boolean;
|
||||
|
||||
@FieldMetadata({
|
||||
standardId: calendarEventAttendeeStandardFieldIds.responseStatus,
|
||||
type: FieldMetadataType.SELECT,
|
||||
label: 'Response Status',
|
||||
description: 'Response Status',
|
||||
icon: 'IconUser',
|
||||
options: [
|
||||
{
|
||||
value: CalendarEventAttendeeResponseStatus.NEEDS_ACTION,
|
||||
label: 'Needs Action',
|
||||
position: 0,
|
||||
color: 'orange',
|
||||
},
|
||||
{
|
||||
value: CalendarEventAttendeeResponseStatus.DECLINED,
|
||||
label: 'Declined',
|
||||
position: 1,
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
value: CalendarEventAttendeeResponseStatus.TENTATIVE,
|
||||
label: 'Tentative',
|
||||
position: 2,
|
||||
color: 'yellow',
|
||||
},
|
||||
{
|
||||
value: CalendarEventAttendeeResponseStatus.ACCEPTED,
|
||||
label: 'Accepted',
|
||||
position: 3,
|
||||
color: 'green',
|
||||
},
|
||||
],
|
||||
defaultValue: { value: CalendarEventAttendeeResponseStatus.NEEDS_ACTION },
|
||||
})
|
||||
responseStatus: string;
|
||||
|
||||
@FieldMetadata({
|
||||
standardId: calendarEventAttendeeStandardFieldIds.person,
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Person',
|
||||
description: 'Person',
|
||||
icon: 'IconUser',
|
||||
joinColumn: 'personId',
|
||||
})
|
||||
@IsNullable()
|
||||
person: PersonObjectMetadata;
|
||||
|
||||
@FieldMetadata({
|
||||
standardId: calendarEventAttendeeStandardFieldIds.workspaceMember,
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Workspace Member',
|
||||
description: 'Workspace Member',
|
||||
icon: 'IconUser',
|
||||
joinColumn: 'workspaceMemberId',
|
||||
})
|
||||
@IsNullable()
|
||||
workspaceMember: WorkspaceMemberObjectMetadata;
|
||||
}
|
||||
@ -0,0 +1,184 @@
|
||||
import { RelationMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/relation-metadata.decorator';
|
||||
import { FeatureFlagKeys } from 'src/engine/modules/feature-flag/feature-flag.entity';
|
||||
import { FieldMetadataType } from 'src/engine-metadata/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
RelationMetadataType,
|
||||
RelationOnDeleteAction,
|
||||
} from 'src/engine-metadata/relation-metadata/relation-metadata.entity';
|
||||
import { FieldMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/field-metadata.decorator';
|
||||
import { Gate } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/gate.decorator';
|
||||
import { IsSystem } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/is-system.decorator';
|
||||
import { ObjectMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/object-metadata.decorator';
|
||||
import { BaseObjectMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects/base.object-metadata';
|
||||
import { CalendarChannelEventAssociationObjectMetadata } from 'src/modules/calendar/standard-objects/calendar-channel-event-association.object-metadata';
|
||||
import { CalendarEventAttendeeObjectMetadata } from 'src/modules/calendar/standard-objects/calendar-event-attendee.object-metadata';
|
||||
import { IsNullable } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/is-nullable.decorator';
|
||||
import { standardObjectIds } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||
import { calendarEventStandardFieldIds } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
|
||||
@ObjectMetadata({
|
||||
standardId: standardObjectIds.calendarEvent,
|
||||
namePlural: 'calendarEvents',
|
||||
labelSingular: 'Calendar event',
|
||||
labelPlural: 'Calendar events',
|
||||
description: 'Calendar events',
|
||||
icon: 'IconCalendar',
|
||||
})
|
||||
@IsSystem()
|
||||
@Gate({
|
||||
featureFlag: FeatureFlagKeys.IsCalendarEnabled,
|
||||
})
|
||||
export class CalendarEventObjectMetadata extends BaseObjectMetadata {
|
||||
@FieldMetadata({
|
||||
standardId: calendarEventStandardFieldIds.title,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Title',
|
||||
description: 'Title',
|
||||
icon: 'IconH1',
|
||||
})
|
||||
title: string;
|
||||
|
||||
@FieldMetadata({
|
||||
standardId: calendarEventStandardFieldIds.isCanceled,
|
||||
type: FieldMetadataType.BOOLEAN,
|
||||
label: 'Is canceled',
|
||||
description: 'Is canceled',
|
||||
icon: 'IconCalendarCancel',
|
||||
})
|
||||
isCanceled: boolean;
|
||||
|
||||
@FieldMetadata({
|
||||
standardId: calendarEventStandardFieldIds.isFullDay,
|
||||
type: FieldMetadataType.BOOLEAN,
|
||||
label: 'Is Full Day',
|
||||
description: 'Is Full Day',
|
||||
icon: 'Icon24Hours',
|
||||
})
|
||||
isFullDay: boolean;
|
||||
|
||||
@FieldMetadata({
|
||||
standardId: calendarEventStandardFieldIds.startsAt,
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
label: 'Start DateTime',
|
||||
description: 'Start DateTime',
|
||||
icon: 'IconCalendarClock',
|
||||
})
|
||||
@IsNullable()
|
||||
startsAt: string | null;
|
||||
|
||||
@FieldMetadata({
|
||||
standardId: calendarEventStandardFieldIds.endsAt,
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
label: 'End DateTime',
|
||||
description: 'End DateTime',
|
||||
icon: 'IconCalendarClock',
|
||||
})
|
||||
@IsNullable()
|
||||
endsAt: string | null;
|
||||
|
||||
@FieldMetadata({
|
||||
standardId: calendarEventStandardFieldIds.externalCreatedAt,
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
label: 'Creation DateTime',
|
||||
description: 'Creation DateTime',
|
||||
icon: 'IconCalendarPlus',
|
||||
})
|
||||
@IsNullable()
|
||||
externalCreatedAt: string | null;
|
||||
|
||||
@FieldMetadata({
|
||||
standardId: calendarEventStandardFieldIds.externalUpdatedAt,
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
label: 'Update DateTime',
|
||||
description: 'Update DateTime',
|
||||
icon: 'IconCalendarCog',
|
||||
})
|
||||
@IsNullable()
|
||||
externalUpdatedAt: string | null;
|
||||
|
||||
@FieldMetadata({
|
||||
standardId: calendarEventStandardFieldIds.description,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Description',
|
||||
description: 'Description',
|
||||
icon: 'IconFileDescription',
|
||||
})
|
||||
description: string;
|
||||
|
||||
@FieldMetadata({
|
||||
standardId: calendarEventStandardFieldIds.location,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Location',
|
||||
description: 'Location',
|
||||
icon: 'IconMapPin',
|
||||
})
|
||||
location: string;
|
||||
|
||||
@FieldMetadata({
|
||||
standardId: calendarEventStandardFieldIds.iCalUID,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'iCal UID',
|
||||
description: 'iCal UID',
|
||||
icon: 'IconKey',
|
||||
})
|
||||
iCalUID: string;
|
||||
|
||||
@FieldMetadata({
|
||||
standardId: calendarEventStandardFieldIds.conferenceSolution,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Conference Solution',
|
||||
description: 'Conference Solution',
|
||||
icon: 'IconScreenShare',
|
||||
})
|
||||
conferenceSolution: string;
|
||||
|
||||
@FieldMetadata({
|
||||
standardId: calendarEventStandardFieldIds.conferenceUri,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Conference URI',
|
||||
description: 'Conference URI',
|
||||
icon: 'IconLink',
|
||||
})
|
||||
conferenceUri: string;
|
||||
|
||||
@FieldMetadata({
|
||||
standardId: calendarEventStandardFieldIds.recurringEventExternalId,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Recurring Event ID',
|
||||
description: 'Recurring Event ID',
|
||||
icon: 'IconHistory',
|
||||
})
|
||||
recurringEventExternalId: string;
|
||||
|
||||
@FieldMetadata({
|
||||
standardId: calendarEventStandardFieldIds.calendarChannelEventAssociations,
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Calendar Channel Event Associations',
|
||||
description: 'Calendar Channel Event Associations',
|
||||
icon: 'IconCalendar',
|
||||
})
|
||||
@RelationMetadata({
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
inverseSideTarget: () => CalendarChannelEventAssociationObjectMetadata,
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
inverseSideFieldKey: 'calendarEvent',
|
||||
})
|
||||
@Gate({
|
||||
featureFlag: 'IS_CALENDAR_ENABLED',
|
||||
})
|
||||
calendarChannelEventAssociations: CalendarChannelEventAssociationObjectMetadata[];
|
||||
|
||||
@FieldMetadata({
|
||||
standardId: calendarEventStandardFieldIds.eventAttendees,
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Event Attendees',
|
||||
description: 'Event Attendees',
|
||||
icon: 'IconUserCircle',
|
||||
})
|
||||
@RelationMetadata({
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
inverseSideTarget: () => CalendarEventAttendeeObjectMetadata,
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
})
|
||||
eventAttendees: CalendarEventAttendeeObjectMetadata[];
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
import { CalendarEventAttendeeObjectMetadata } from 'src/modules/calendar/standard-objects/calendar-event-attendee.object-metadata';
|
||||
import { CalendarEventObjectMetadata } from 'src/modules/calendar/standard-objects/calendar-event.object-metadata';
|
||||
import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record';
|
||||
|
||||
export type CalendarEvent = Omit<
|
||||
ObjectRecord<CalendarEventObjectMetadata>,
|
||||
| 'createdAt'
|
||||
| 'updatedAt'
|
||||
| 'calendarChannelEventAssociations'
|
||||
| 'calendarEventAttendees'
|
||||
| 'eventAttendees'
|
||||
>;
|
||||
|
||||
export type CalendarEventAttendee = Omit<
|
||||
ObjectRecord<CalendarEventAttendeeObjectMetadata>,
|
||||
| 'id'
|
||||
| 'createdAt'
|
||||
| 'updatedAt'
|
||||
| 'personId'
|
||||
| 'workspaceMemberId'
|
||||
| 'person'
|
||||
| 'workspaceMember'
|
||||
| 'calendarEvent'
|
||||
> & {
|
||||
iCalUID: string;
|
||||
};
|
||||
|
||||
export type CalendarEventWithAttendees = CalendarEvent & {
|
||||
externalId: string;
|
||||
attendees: CalendarEventAttendee[];
|
||||
};
|
||||
@ -0,0 +1,52 @@
|
||||
import { calendar_v3 } from 'googleapis';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { CalendarEventWithAttendees } from 'src/modules/calendar/types/calendar-event';
|
||||
import { CalendarEventAttendeeResponseStatus } from 'src/modules/calendar/standard-objects/calendar-event-attendee.object-metadata';
|
||||
|
||||
export const formatGoogleCalendarEvent = (
|
||||
event: calendar_v3.Schema$Event,
|
||||
): CalendarEventWithAttendees => {
|
||||
const id = v4();
|
||||
|
||||
const formatResponseStatus = (status: string | null | undefined) => {
|
||||
switch (status) {
|
||||
case 'accepted':
|
||||
return CalendarEventAttendeeResponseStatus.ACCEPTED;
|
||||
case 'declined':
|
||||
return CalendarEventAttendeeResponseStatus.DECLINED;
|
||||
case 'tentative':
|
||||
return CalendarEventAttendeeResponseStatus.TENTATIVE;
|
||||
default:
|
||||
return CalendarEventAttendeeResponseStatus.NEEDS_ACTION;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
id,
|
||||
title: event.summary ?? '',
|
||||
isCanceled: event.status === 'cancelled',
|
||||
isFullDay: event.start?.dateTime == null,
|
||||
startsAt: event.start?.dateTime ?? event.start?.date ?? null,
|
||||
endsAt: event.end?.dateTime ?? event.end?.date ?? null,
|
||||
externalId: event.id ?? '',
|
||||
externalCreatedAt: event.created ?? null,
|
||||
externalUpdatedAt: event.updated ?? null,
|
||||
description: event.description ?? '',
|
||||
location: event.location ?? '',
|
||||
iCalUID: event.iCalUID ?? '',
|
||||
conferenceSolution:
|
||||
event.conferenceData?.conferenceSolution?.key?.type ?? '',
|
||||
conferenceUri: event.conferenceData?.entryPoints?.[0]?.uri ?? '',
|
||||
recurringEventExternalId: event.recurringEventId ?? '',
|
||||
attendees:
|
||||
event.attendees?.map((attendee) => ({
|
||||
calendarEventId: id,
|
||||
iCalUID: event.iCalUID ?? '',
|
||||
handle: attendee.email ?? '',
|
||||
displayName: attendee.displayName ?? '',
|
||||
isOrganizer: attendee.organizer === true,
|
||||
responseStatus: formatResponseStatus(attendee.responseStatus),
|
||||
})) ?? [],
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,56 @@
|
||||
export const valuesStringForBatchRawQuery = (
|
||||
values: {
|
||||
[key: string]: any;
|
||||
}[],
|
||||
typesArray: string[] = [],
|
||||
) => {
|
||||
const castedValues = values.reduce((acc, _, rowIndex) => {
|
||||
const numberOfColumns = typesArray.length;
|
||||
|
||||
const rowValues = Array.from(
|
||||
{ length: numberOfColumns },
|
||||
(_, columnIndex) => {
|
||||
const placeholder = `$${rowIndex * numberOfColumns + columnIndex + 1}`;
|
||||
const typeCast = typesArray[columnIndex]
|
||||
? `::${typesArray[columnIndex]}`
|
||||
: '';
|
||||
|
||||
return `${placeholder}${typeCast}`;
|
||||
},
|
||||
).join(', ');
|
||||
|
||||
acc.push(`(${rowValues})`);
|
||||
|
||||
return acc;
|
||||
}, [] as string[]);
|
||||
|
||||
return castedValues.join(', ');
|
||||
};
|
||||
|
||||
export const getFlattenedValuesAndValuesStringForBatchRawQuery = (
|
||||
values: {
|
||||
[key: string]: any;
|
||||
}[],
|
||||
keyTypeMap: {
|
||||
[key: string]: string;
|
||||
},
|
||||
): {
|
||||
flattenedValues: any[];
|
||||
valuesString: string;
|
||||
} => {
|
||||
const keysToInsert = Object.keys(keyTypeMap);
|
||||
|
||||
const flattenedValues = values.flatMap((value) =>
|
||||
keysToInsert.map((key) => value[key]),
|
||||
);
|
||||
|
||||
const valuesString = valuesStringForBatchRawQuery(
|
||||
values,
|
||||
Object.values(keyTypeMap),
|
||||
);
|
||||
|
||||
return {
|
||||
flattenedValues,
|
||||
valuesString,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,9 @@
|
||||
export const googleCalendarSearchFilterExcludeEmails = (
|
||||
emails: string[],
|
||||
): string => {
|
||||
if (emails.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return `email=-${emails.join(', -')}`;
|
||||
};
|
||||
Reference in New Issue
Block a user