4285 timebox create google calendar full sync (#4442)

* calendar module

* wip

* creating a folder for common files between calendar and messages

* wip

* wip

* wip

* wip

* update calendar search filter

* wip

* working on full sync service

* reorganizing folders

* adding repositories

* fix typo

* working on full-sync service

* Add calendarQueue to MessageQueue enum and update dependencies

* start transaction

* wip

* add save and update functions for event

* wip

* save events

* improving step by step

* add calendar scope

* fix nest modules imports

* renaming

* create calendar channel

* create job for google calendar full-sync

* call GoogleCalendarFullSyncJob after connected account creation

* ask for scope conditionnally

* fixes

* create channels conditionnally

* fix

* fixes

* fix FK bug

* filter out canceled events

* create save and update functions for calendarEventAttendee repository

* saving messageParticipants is working

* save calendarEventAttendees is working

* add calendarEvent cleaner

* calendar event cleaner is working

* working on updating attendees

* wip

* reintroducing google-gmail endpoint to ensure smooth deploy

* modify callbackURL

* modify front url

* changes to be able to merge

* put back feature flag

* fixes after PR comments

* add feature flag check

* remove unused modules

* separate delete connected account associated job data in two jobs

* fix error

* rename calendar_v3 as calendarV3

* Update packages/twenty-server/src/workspace/calendar-and-messaging/utils/valueStringForBatchRawQuery.util.ts

Co-authored-by: Jérémy M <jeremy.magrin@gmail.com>

* improve readability

* renaming to remove plural

* renaming to remove plural

* don't throw if no connected account is found

* use calendar queue

* modify usage of HttpService in fetch-by-batch

* modify valuesStringForBatchRawQuery to improve api and return flattened values

* fix auth module feature flag import

* fix getFlattenedValuesAndValuesStringForBatchRawQuery

---------

Co-authored-by: Jérémy M <jeremy.magrin@gmail.com>
This commit is contained in:
bosiraphael
2024-03-14 11:23:31 +01:00
committed by GitHub
parent e0dac82e07
commit 3caf860848
76 changed files with 1856 additions and 280 deletions

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { BlocklistService } from 'src/workspace/calendar-and-messaging/repositories/blocklist/blocklist.service';
import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module';
@Module({
imports: [WorkspaceDataSourceModule],
providers: [BlocklistService],
exports: [BlocklistService],
})
export class BlocklistModule {}

View File

@ -0,0 +1,30 @@
import { Injectable } from '@nestjs/common';
import { EntityManager } from 'typeorm';
import { WorkspaceDataSourceService } from 'src/workspace/workspace-datasource/workspace-datasource.service';
import { ObjectRecord } from 'src/workspace/workspace-sync-metadata/types/object-record';
import { BlocklistObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/blocklist.object-metadata';
@Injectable()
export class BlocklistService {
constructor(
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
) {}
public async getByWorkspaceMemberId(
workspaceMemberId: string,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<ObjectRecord<BlocklistObjectMetadata>[]> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
return await this.workspaceDataSourceService.executeRawQuery(
`SELECT * FROM ${dataSourceSchema}."blocklist" WHERE "workspaceMemberId" = $1`,
[workspaceMemberId],
workspaceId,
transactionManager,
);
}
}

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { ConnectedAccountService } from 'src/workspace/calendar-and-messaging/repositories/connected-account/connected-account.service';
import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module';
@Module({
imports: [WorkspaceDataSourceModule],
providers: [ConnectedAccountService],
exports: [ConnectedAccountService],
})
export class ConnectedAccountModule {}

View File

@ -0,0 +1,153 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { EntityManager } from 'typeorm';
import { WorkspaceDataSourceService } from 'src/workspace/workspace-datasource/workspace-datasource.service';
import { ConnectedAccountObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/connected-account.object-metadata';
import { ObjectRecord } from 'src/workspace/workspace-sync-metadata/types/object-record';
@Injectable()
export class ConnectedAccountService {
constructor(
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
) {}
public async getAll(
workspaceId: string,
transactionManager?: EntityManager,
): Promise<ObjectRecord<ConnectedAccountObjectMetadata>[]> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
return await this.workspaceDataSourceService.executeRawQuery(
`SELECT * FROM ${dataSourceSchema}."connectedAccount" WHERE "provider" = 'google'`,
[],
workspaceId,
transactionManager,
);
}
public async getByIds(
connectedAccountIds: string[],
workspaceId: string,
transactionManager?: EntityManager,
): Promise<ObjectRecord<ConnectedAccountObjectMetadata>[]> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
return await this.workspaceDataSourceService.executeRawQuery(
`SELECT * FROM ${dataSourceSchema}."connectedAccount" WHERE "id" = ANY($1)`,
[connectedAccountIds],
workspaceId,
transactionManager,
);
}
public async getById(
connectedAccountId: string,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<ObjectRecord<ConnectedAccountObjectMetadata> | undefined> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
const connectedAccounts =
await this.workspaceDataSourceService.executeRawQuery(
`SELECT * FROM ${dataSourceSchema}."connectedAccount" WHERE "id" = $1 LIMIT 1`,
[connectedAccountId],
workspaceId,
transactionManager,
);
return connectedAccounts[0];
}
public async getByIdOrFail(
connectedAccountId: string,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<ObjectRecord<ConnectedAccountObjectMetadata>> {
const connectedAccount = await this.getById(
connectedAccountId,
workspaceId,
transactionManager,
);
if (!connectedAccount) {
throw new NotFoundException(
`Connected account with id ${connectedAccountId} not found in workspace ${workspaceId}`,
);
}
return connectedAccount;
}
public async updateLastSyncHistoryId(
historyId: string,
connectedAccountId: string,
workspaceId: string,
transactionManager?: EntityManager,
) {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
await this.workspaceDataSourceService.executeRawQuery(
`UPDATE ${dataSourceSchema}."connectedAccount" SET "lastSyncHistoryId" = $1 WHERE "id" = $2`,
[historyId, connectedAccountId],
workspaceId,
transactionManager,
);
}
public async updateLastSyncHistoryIdIfHigher(
historyId: string,
connectedAccountId: string,
workspaceId: string,
transactionManager?: EntityManager,
) {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
await this.workspaceDataSourceService.executeRawQuery(
`UPDATE ${dataSourceSchema}."connectedAccount" SET "lastSyncHistoryId" = $1
WHERE "id" = $2
AND ("lastSyncHistoryId" < $1 OR "lastSyncHistoryId" = '')`,
[historyId, connectedAccountId],
workspaceId,
transactionManager,
);
}
public async deleteHistoryId(
connectedAccountId: string,
workspaceId: string,
transactionManager?: EntityManager,
) {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
await this.workspaceDataSourceService.executeRawQuery(
`UPDATE ${dataSourceSchema}."connectedAccount" SET "lastSyncHistoryId" = '' WHERE "id" = $1`,
[connectedAccountId],
workspaceId,
transactionManager,
);
}
public async updateAccessToken(
accessToken: string,
connectedAccountId: string,
workspaceId: string,
transactionManager?: EntityManager,
) {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
await this.workspaceDataSourceService.executeRawQuery(
`UPDATE ${dataSourceSchema}."connectedAccount" SET "accessToken" = $1 WHERE "id" = $2`,
[accessToken, connectedAccountId],
workspaceId,
transactionManager,
);
}
}

View File

@ -0,0 +1,65 @@
import { Injectable } from '@nestjs/common';
import axios from 'axios';
import { EnvironmentService } from 'src/integrations/environment/environment.service';
import { ConnectedAccountService } from 'src/workspace/calendar-and-messaging/repositories/connected-account/connected-account.service';
@Injectable()
export class GoogleAPIsRefreshAccessTokenService {
constructor(
private readonly environmentService: EnvironmentService,
private readonly connectedAccountService: ConnectedAccountService,
) {}
async refreshAndSaveAccessToken(
workspaceId: string,
connectedAccountId: string,
): Promise<void> {
const connectedAccount = await this.connectedAccountService.getById(
connectedAccountId,
workspaceId,
);
if (!connectedAccount) {
throw new Error(
`No connected account found for ${connectedAccountId} in workspace ${workspaceId}`,
);
}
const refreshToken = connectedAccount.refreshToken;
if (!refreshToken) {
throw new Error(
`No refresh token found for connected account ${connectedAccountId} in workspace ${workspaceId}`,
);
}
const accessToken = await this.refreshAccessToken(refreshToken);
await this.connectedAccountService.updateAccessToken(
accessToken,
connectedAccountId,
workspaceId,
);
}
async refreshAccessToken(refreshToken: string): Promise<string> {
const response = await axios.post(
'https://oauth2.googleapis.com/token',
{
client_id: this.environmentService.getAuthGoogleClientId(),
client_secret: this.environmentService.getAuthGoogleClientSecret(),
refresh_token: refreshToken,
grant_type: 'refresh_token',
},
{
headers: {
'Content-Type': 'application/json',
},
},
);
return response.data.access_token;
}
}

View File

@ -0,0 +1,5 @@
type Query = {
uri: string;
};
export type BatchQueries = Query[];

View File

@ -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,
};
};