Migrate gmail fetch by batch to library (#17514)
Replace manual HTTP batch implementation with `@jrmdayn/googleapis-batcher` library (we already use this for fetching message list) Initial real testing works fine but do not merge yet needs more extensive real test runs
This commit is contained in:
-6
@@ -1,4 +1,3 @@
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
@@ -11,7 +10,6 @@ import { BlocklistWorkspaceEntity } from 'src/modules/blocklist/standard-objects
|
||||
import { EmailAliasManagerModule } from 'src/modules/connected-account/email-alias-manager/email-alias-manager.module';
|
||||
import { OAuth2ClientManagerModule } from 'src/modules/connected-account/oauth2-client-manager/oauth2-client-manager.module';
|
||||
import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module';
|
||||
import { GmailFetchByBatchService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-fetch-by-batch.service';
|
||||
import { GmailGetHistoryService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-history.service';
|
||||
import { GmailGetMessageListService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-message-list.service';
|
||||
import { GmailGetMessagesService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-messages.service';
|
||||
@@ -21,9 +19,6 @@ import { MessageParticipantManagerModule } from 'src/modules/messaging/message-p
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
HttpModule.register({
|
||||
baseURL: 'https://www.googleapis.com/batch/gmail/v1',
|
||||
}),
|
||||
TwentyConfigModule,
|
||||
ObjectMetadataRepositoryModule.forFeature([BlocklistWorkspaceEntity]),
|
||||
MessagingCommonModule,
|
||||
@@ -36,7 +31,6 @@ import { MessageParticipantManagerModule } from 'src/modules/messaging/message-p
|
||||
],
|
||||
providers: [
|
||||
GmailGetHistoryService,
|
||||
GmailFetchByBatchService,
|
||||
GmailGetMessagesService,
|
||||
GmailGetMessageListService,
|
||||
GmailMessageListFetchErrorHandler,
|
||||
|
||||
-217
@@ -1,217 +0,0 @@
|
||||
import { type GmailApiBatchError } from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-api-batch-error.type';
|
||||
|
||||
const gmailBatchApiErrorMocks = {
|
||||
// 400 Bad Request - Invalid query parameters
|
||||
badRequest: {
|
||||
code: 400,
|
||||
errors: [
|
||||
{
|
||||
domain: 'global',
|
||||
location: 'orderBy',
|
||||
locationType: 'parameter',
|
||||
message:
|
||||
'Sorting is not supported for queries with fullText terms. Results are always in descending relevance order.',
|
||||
reason: 'badRequest',
|
||||
},
|
||||
],
|
||||
message:
|
||||
'Sorting is not supported for queries with fullText terms. Results are always in descending relevance order.',
|
||||
},
|
||||
|
||||
// 400 Invalid Grant
|
||||
invalidGrant: {
|
||||
code: 400,
|
||||
errors: [
|
||||
{
|
||||
domain: 'global',
|
||||
reason: 'invalid_grant',
|
||||
message: 'Invalid Credentials',
|
||||
},
|
||||
],
|
||||
message: 'Invalid Credentials',
|
||||
},
|
||||
|
||||
// 400 Failed Precondition
|
||||
failedPrecondition: {
|
||||
code: 400,
|
||||
errors: [
|
||||
{
|
||||
domain: 'global',
|
||||
reason: 'failedPrecondition',
|
||||
message: 'Failed Precondition',
|
||||
},
|
||||
],
|
||||
message: 'Failed Precondition',
|
||||
},
|
||||
|
||||
// 401 Invalid Credentials
|
||||
invalidCredentials: {
|
||||
errors: [
|
||||
{
|
||||
domain: 'global',
|
||||
reason: 'authError',
|
||||
message: 'Invalid Credentials',
|
||||
locationType: 'header',
|
||||
location: 'Authorization',
|
||||
},
|
||||
],
|
||||
code: 401,
|
||||
message: 'Invalid Credentials',
|
||||
},
|
||||
|
||||
// 404 Not Found
|
||||
notFound: {
|
||||
errors: [
|
||||
{
|
||||
domain: 'global',
|
||||
reason: 'notFound',
|
||||
message: 'Resource not found: userId',
|
||||
location: 'userId',
|
||||
locationType: 'parameter',
|
||||
},
|
||||
],
|
||||
code: 404,
|
||||
message: 'Resource not found: userId',
|
||||
},
|
||||
|
||||
// 410 Gone
|
||||
gone: {
|
||||
errors: [
|
||||
{
|
||||
domain: 'global',
|
||||
reason: 'resourceGone',
|
||||
message: 'Resource has been deleted',
|
||||
location: 'messageId',
|
||||
locationType: 'parameter',
|
||||
},
|
||||
],
|
||||
code: 410,
|
||||
message: 'Resource has been deleted',
|
||||
},
|
||||
|
||||
// 403 Daily Limit Exceeded
|
||||
dailyLimitExceeded: {
|
||||
errors: [
|
||||
{
|
||||
domain: 'usageLimits',
|
||||
reason: 'dailyLimitExceeded',
|
||||
message: 'Daily Limit Exceeded',
|
||||
},
|
||||
],
|
||||
code: 403,
|
||||
message: 'Daily Limit Exceeded',
|
||||
},
|
||||
|
||||
// 403 User Rate Limit Exceeded
|
||||
userRateLimitExceeded: {
|
||||
errors: [
|
||||
{
|
||||
domain: 'usageLimits',
|
||||
reason: 'userRateLimitExceeded',
|
||||
message: 'User Rate Limit Exceeded',
|
||||
},
|
||||
],
|
||||
code: 403,
|
||||
message: 'User Rate Limit Exceeded',
|
||||
},
|
||||
|
||||
// 403 Rate Limit Exceeded
|
||||
rateLimitExceeded: {
|
||||
errors: [
|
||||
{
|
||||
domain: 'usageLimits',
|
||||
reason: 'rateLimitExceeded',
|
||||
message: 'Rate Limit Exceeded',
|
||||
},
|
||||
],
|
||||
code: 403,
|
||||
message: 'Rate Limit Exceeded',
|
||||
},
|
||||
|
||||
// 403 Domain Policy Error
|
||||
domainPolicyError: {
|
||||
errors: [
|
||||
{
|
||||
domain: 'global',
|
||||
reason: 'domainPolicy',
|
||||
message: 'The domain administrators have disabled Gmail apps.',
|
||||
},
|
||||
],
|
||||
code: 403,
|
||||
message: 'The domain administrators have disabled Gmail apps.',
|
||||
},
|
||||
|
||||
// 429 Too Many Requests (Concurrent Requests)
|
||||
tooManyConcurrentRequests: {
|
||||
errors: [
|
||||
{
|
||||
domain: 'global',
|
||||
reason: 'rateLimitExceeded',
|
||||
message: 'Too many concurrent requests for user',
|
||||
},
|
||||
],
|
||||
code: 429,
|
||||
message: 'Too many concurrent requests for user',
|
||||
},
|
||||
|
||||
// 500 Backend Error
|
||||
backendError: {
|
||||
errors: [
|
||||
{
|
||||
domain: 'global',
|
||||
reason: 'backendError',
|
||||
message: 'Backend Error',
|
||||
},
|
||||
],
|
||||
code: 500,
|
||||
message: 'Backend Error',
|
||||
},
|
||||
|
||||
getError: function (code: number, type?: string): GmailApiBatchError {
|
||||
switch (code) {
|
||||
case 400:
|
||||
switch (type) {
|
||||
case 'invalid_grant':
|
||||
return this.invalidGrant;
|
||||
case 'failedPrecondition':
|
||||
return this.failedPrecondition;
|
||||
default:
|
||||
return this.badRequest;
|
||||
}
|
||||
case 401:
|
||||
return this.invalidCredentials;
|
||||
case 403:
|
||||
switch (type) {
|
||||
case 'dailyLimit':
|
||||
return this.dailyLimitExceeded;
|
||||
case 'userRateLimit':
|
||||
return this.userRateLimitExceeded;
|
||||
case 'rateLimit':
|
||||
return this.rateLimitExceeded;
|
||||
case 'domainPolicy':
|
||||
return this.domainPolicyError;
|
||||
default:
|
||||
return this.rateLimitExceeded;
|
||||
}
|
||||
case 404:
|
||||
return this.notFound;
|
||||
case 410:
|
||||
return this.gone;
|
||||
case 429:
|
||||
switch (type) {
|
||||
case 'concurrent':
|
||||
return this.tooManyConcurrentRequests;
|
||||
case 'mailSending':
|
||||
return this.mailSendingLimitExceeded;
|
||||
default:
|
||||
return this.tooManyConcurrentRequests;
|
||||
}
|
||||
case 500:
|
||||
return this.backendError;
|
||||
default:
|
||||
throw new Error(`Unknown error code: ${code}`);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default gmailBatchApiErrorMocks;
|
||||
-147
@@ -1,147 +0,0 @@
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type AxiosResponse } from 'axios';
|
||||
|
||||
import { type GmailMessageParsedResponse } from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-message-parsed-response.type';
|
||||
import { createQueriesFromMessageIds } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/create-queries-from-message-ids.util';
|
||||
import { type BatchQueries } from 'src/modules/messaging/message-import-manager/types/batch-queries';
|
||||
|
||||
@Injectable()
|
||||
export class GmailFetchByBatchService {
|
||||
constructor(private readonly httpService: HttpService) {}
|
||||
|
||||
async fetchAllByBatches(
|
||||
messageIds: string[],
|
||||
accessToken: string,
|
||||
boundary: string,
|
||||
): Promise<{
|
||||
messageIdsByBatch: string[][];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
batchResponses: AxiosResponse<any, any>[];
|
||||
}> {
|
||||
const batchLimit = 20;
|
||||
|
||||
let batchOffset = 0;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let batchResponses: AxiosResponse<any, any>[] = [];
|
||||
|
||||
const messageIdsByBatch: string[][] = [];
|
||||
|
||||
while (batchOffset < messageIds.length) {
|
||||
const batchResponse = await this.fetchBatch(
|
||||
messageIds,
|
||||
accessToken,
|
||||
batchOffset,
|
||||
batchLimit,
|
||||
boundary,
|
||||
);
|
||||
|
||||
batchResponses = batchResponses.concat(batchResponse);
|
||||
|
||||
messageIdsByBatch.push(
|
||||
messageIds.slice(batchOffset, batchOffset + batchLimit),
|
||||
);
|
||||
|
||||
batchOffset += batchLimit;
|
||||
}
|
||||
|
||||
return { messageIdsByBatch, batchResponses };
|
||||
}
|
||||
|
||||
async fetchBatch(
|
||||
messageIds: string[],
|
||||
accessToken: string,
|
||||
batchOffset: number,
|
||||
batchLimit: number,
|
||||
boundary: string,
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
): Promise<AxiosResponse<any, any>> {
|
||||
const queries = createQueriesFromMessageIds(messageIds);
|
||||
|
||||
const limitedQueries = queries.slice(batchOffset, batchOffset + batchLimit);
|
||||
|
||||
const response = await this.httpService.axiosRef.post(
|
||||
'/',
|
||||
this.createBatchBody(limitedQueries, boundary),
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/mixed; boundary=' + boundary,
|
||||
Authorization: 'Bearer ' + accessToken,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
createBatchBody(queries: BatchQueries, boundary: string): string {
|
||||
let batchBody: string[] = [];
|
||||
|
||||
queries.forEach(function (call) {
|
||||
const method = 'GET';
|
||||
const uri = call.uri;
|
||||
|
||||
batchBody = batchBody.concat([
|
||||
'--',
|
||||
boundary,
|
||||
'\r\n',
|
||||
'Content-Type: application/http',
|
||||
'\r\n\r\n',
|
||||
|
||||
method,
|
||||
' ',
|
||||
uri,
|
||||
'\r\n\r\n',
|
||||
]);
|
||||
});
|
||||
|
||||
return batchBody.concat(['--', boundary, '--']).join('');
|
||||
}
|
||||
|
||||
parseBatch(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
responseCollection: AxiosResponse<any, any>,
|
||||
): GmailMessageParsedResponse[] {
|
||||
const responseItems: GmailMessageParsedResponse[] = [];
|
||||
|
||||
const boundary = this.getBatchSeparator(responseCollection);
|
||||
|
||||
const responseLines: string[] = responseCollection.data.split(
|
||||
'--' + boundary,
|
||||
);
|
||||
|
||||
responseLines.forEach(function (response) {
|
||||
const startJson = response.indexOf('{');
|
||||
const endJson = response.lastIndexOf('}');
|
||||
|
||||
if (startJson < 0 || endJson < 0) return;
|
||||
|
||||
const responseJson = response.substring(startJson, endJson + 1);
|
||||
|
||||
const item = JSON.parse(responseJson);
|
||||
|
||||
responseItems.push(item);
|
||||
});
|
||||
|
||||
return responseItems;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
getBatchSeparator(responseCollection: AxiosResponse<any, any>): string {
|
||||
const headers = responseCollection.headers;
|
||||
|
||||
const contentType: string = headers['content-type'];
|
||||
|
||||
if (!contentType) return '';
|
||||
|
||||
const components = contentType.split('; ');
|
||||
|
||||
const boundary = components.find((item) => item.startsWith('boundary='));
|
||||
|
||||
return boundary?.replace('boundary=', '').trim() || '';
|
||||
}
|
||||
}
|
||||
+50
-58
@@ -1,23 +1,21 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type AxiosResponse } from 'axios';
|
||||
import { type gmail_v1 as gmailV1 } from 'googleapis';
|
||||
import { batchFetchImplementation } from '@jrmdayn/googleapis-batcher';
|
||||
import { type gmail_v1 as gmailV1, google } from 'googleapis';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
|
||||
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import {
|
||||
MessageImportDriverException,
|
||||
MessageImportDriverExceptionCode,
|
||||
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
|
||||
import { GmailFetchByBatchService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-fetch-by-batch.service';
|
||||
import { GmailMessagesImportErrorHandler } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-messages-import-error-handler.service';
|
||||
import { parseAndFormatGmailMessage } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-and-format-gmail-message.util';
|
||||
import { type MessageWithParticipants } from 'src/modules/messaging/message-import-manager/types/message';
|
||||
|
||||
const GMAIL_BATCH_REQUEST_MAX_SIZE = 50;
|
||||
|
||||
@Injectable()
|
||||
export class GmailGetMessagesService {
|
||||
constructor(
|
||||
private readonly fetchByBatchesService: GmailFetchByBatchService,
|
||||
private readonly oAuth2ClientManagerService: OAuth2ClientManagerService,
|
||||
private readonly gmailMessagesImportErrorHandler: GmailMessagesImportErrorHandler,
|
||||
) {}
|
||||
|
||||
@@ -25,62 +23,56 @@ export class GmailGetMessagesService {
|
||||
messageIds: string[],
|
||||
connectedAccount: Pick<
|
||||
ConnectedAccountWorkspaceEntity,
|
||||
'accessToken' | 'id' | 'handle' | 'handleAliases'
|
||||
| 'provider'
|
||||
| 'accessToken'
|
||||
| 'refreshToken'
|
||||
| 'id'
|
||||
| 'handle'
|
||||
| 'handleAliases'
|
||||
>,
|
||||
): Promise<MessageWithParticipants[]> {
|
||||
if (!isDefined(connectedAccount.accessToken)) {
|
||||
throw new MessageImportDriverException(
|
||||
'Access token is required',
|
||||
MessageImportDriverExceptionCode.ACCESS_TOKEN_MISSING,
|
||||
);
|
||||
}
|
||||
const { messageIdsByBatch, batchResponses } =
|
||||
await this.fetchByBatchesService.fetchAllByBatches(
|
||||
messageIds,
|
||||
connectedAccount.accessToken,
|
||||
'batch_gmail_messages',
|
||||
);
|
||||
|
||||
const messages = batchResponses.flatMap((response, index) => {
|
||||
return this.formatBatchResponseAsMessage(
|
||||
messageIdsByBatch[index],
|
||||
response,
|
||||
const oAuth2Client =
|
||||
await this.oAuth2ClientManagerService.getGoogleOAuth2Client(
|
||||
connectedAccount,
|
||||
);
|
||||
|
||||
const batchedFetchImplementation = batchFetchImplementation({
|
||||
maxBatchSize: GMAIL_BATCH_REQUEST_MAX_SIZE,
|
||||
});
|
||||
|
||||
const batchedGmailClient = google.gmail({
|
||||
version: 'v1',
|
||||
auth: oAuth2Client,
|
||||
fetchImplementation: batchedFetchImplementation,
|
||||
});
|
||||
|
||||
const messagePromises = messageIds.map((messageId) =>
|
||||
batchedGmailClient.users.messages
|
||||
.get({
|
||||
userId: 'me',
|
||||
id: messageId,
|
||||
})
|
||||
.then((response) => ({ messageId, data: response.data, error: null }))
|
||||
.catch((error) => ({ messageId, data: null, error })),
|
||||
);
|
||||
|
||||
const results = await Promise.all(messagePromises);
|
||||
|
||||
const messages = results
|
||||
.map(({ messageId, data, error }) => {
|
||||
if (error) {
|
||||
this.gmailMessagesImportErrorHandler.handleError(error, messageId);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return parseAndFormatGmailMessage(
|
||||
data as gmailV1.Schema$Message,
|
||||
connectedAccount,
|
||||
);
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
private formatBatchResponseAsMessage(
|
||||
messageIds: string[],
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
responseCollection: AxiosResponse<any, any>,
|
||||
connectedAccount: Pick<
|
||||
ConnectedAccountWorkspaceEntity,
|
||||
'handle' | 'handleAliases'
|
||||
>,
|
||||
): MessageWithParticipants[] {
|
||||
const parsedResponses =
|
||||
this.fetchByBatchesService.parseBatch(responseCollection);
|
||||
|
||||
const messages = parsedResponses.map((response, index) => {
|
||||
if ('error' in response) {
|
||||
this.gmailMessagesImportErrorHandler.handleError(
|
||||
response.error,
|
||||
messageIds[index],
|
||||
);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return parseAndFormatGmailMessage(
|
||||
response as gmailV1.Schema$Message,
|
||||
connectedAccount,
|
||||
);
|
||||
});
|
||||
|
||||
return messages.filter(isDefined);
|
||||
}
|
||||
}
|
||||
|
||||
+8
-9
@@ -1,14 +1,12 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
MessageImportDriverException,
|
||||
MessageImportDriverExceptionCode,
|
||||
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
|
||||
import { isGmailApiBatchError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/is-gmail-api-batch-error.util';
|
||||
import { isGmailApiError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/is-gmail-api-error.util';
|
||||
import { isGmailNetworkError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/is-gmail-network-error.util';
|
||||
import { parseGmailApiBatchError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-api-batch-error.util';
|
||||
import { parseGmailApiError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-api-error.util';
|
||||
import { parseGmailNetworkError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-network-error.util';
|
||||
|
||||
@Injectable()
|
||||
@@ -19,21 +17,22 @@ export class GmailMessagesImportErrorHandler {
|
||||
|
||||
public handleError(error: unknown, messageExternalId: string): void {
|
||||
this.logger.error(
|
||||
`Gmail: Error importing messages: ${JSON.stringify(error)}`,
|
||||
`Gmail: Error importing message ${messageExternalId}: ${JSON.stringify(error)}`,
|
||||
);
|
||||
|
||||
if (isGmailNetworkError(error)) {
|
||||
throw parseGmailNetworkError(error);
|
||||
}
|
||||
|
||||
if (isGmailApiBatchError(error)) {
|
||||
const exception = parseGmailApiBatchError(error, messageExternalId);
|
||||
if (isGmailApiError(error)) {
|
||||
const status = error.response?.status;
|
||||
|
||||
if (!isDefined(exception)) {
|
||||
// 404/410 means message was deleted - skip silently
|
||||
if (status === 404 || status === 410) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw exception;
|
||||
throw parseGmailApiError(error);
|
||||
}
|
||||
|
||||
throw new MessageImportDriverException(
|
||||
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
export type GmailApiBatchError = {
|
||||
code: number;
|
||||
errors: {
|
||||
reason: string;
|
||||
message: string;
|
||||
}[];
|
||||
};
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
import { type gmail_v1 } from 'googleapis';
|
||||
|
||||
type GmailMessageError = {
|
||||
error: {
|
||||
code: number;
|
||||
message: string;
|
||||
status: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type GmailMessageParsedResponse =
|
||||
| gmail_v1.Schema$Message
|
||||
| GmailMessageError;
|
||||
+67
-91
@@ -1,149 +1,125 @@
|
||||
import {
|
||||
MessageImportDriverException,
|
||||
MessageImportDriverExceptionCode,
|
||||
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
|
||||
import { default as gmailBatchApiErrorMocks } from 'src/modules/messaging/message-import-manager/drivers/gmail/mocks/gmail-batch-api-error-mocks';
|
||||
import { parseGmailApiBatchError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-api-batch-error.util';
|
||||
import { MessageImportDriverExceptionCode } from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
|
||||
import { getGmailApiError } from 'src/modules/messaging/message-import-manager/drivers/gmail/mocks/gmail-api-error-mocks';
|
||||
import { GmailMessagesImportErrorHandler } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-messages-import-error-handler.service';
|
||||
|
||||
const messageExternalId = '123';
|
||||
|
||||
describe('parseGmailApiBatchError', () => {
|
||||
it('should handle 400 Bad Request', () => {
|
||||
const error = gmailBatchApiErrorMocks.getError(400);
|
||||
const exception = parseGmailApiBatchError(error, messageExternalId);
|
||||
describe('GmailMessagesImportErrorHandler', () => {
|
||||
let handler: GmailMessagesImportErrorHandler;
|
||||
|
||||
expect(exception).toBeInstanceOf(MessageImportDriverException);
|
||||
expect(exception?.code).toBe(MessageImportDriverExceptionCode.UNKNOWN);
|
||||
expect(exception?.message).toBe(
|
||||
`${error.errors[0].message} for message with externalId: ${messageExternalId}`,
|
||||
beforeEach(() => {
|
||||
handler = new GmailMessagesImportErrorHandler();
|
||||
});
|
||||
|
||||
it('should handle 400 Bad Request', () => {
|
||||
const error = getGmailApiError({ code: 400 });
|
||||
|
||||
expect(() => handler.handleError(error, messageExternalId)).toThrow(
|
||||
expect.objectContaining({
|
||||
code: MessageImportDriverExceptionCode.UNKNOWN,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle 400 Invalid Grant', () => {
|
||||
const error = gmailBatchApiErrorMocks.getError(400, 'invalid_grant');
|
||||
const exception = parseGmailApiBatchError(error, messageExternalId);
|
||||
const error = getGmailApiError({ code: 400, reason: 'invalid_grant' });
|
||||
|
||||
expect(exception).toBeInstanceOf(MessageImportDriverException);
|
||||
expect(exception?.code).toBe(
|
||||
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
|
||||
);
|
||||
expect(exception?.message).toBe(
|
||||
`${error.errors[0].message} for message with externalId: ${messageExternalId}`,
|
||||
expect(() => handler.handleError(error, messageExternalId)).toThrow(
|
||||
expect.objectContaining({
|
||||
code: MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle 400 Failed Precondition', () => {
|
||||
const error = gmailBatchApiErrorMocks.getError(400, 'failedPrecondition');
|
||||
const exception = parseGmailApiBatchError(error, messageExternalId);
|
||||
const error = getGmailApiError({ code: 400, reason: 'failedPrecondition' });
|
||||
|
||||
expect(exception).toBeInstanceOf(MessageImportDriverException);
|
||||
expect(exception?.code).toBe(
|
||||
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
|
||||
expect(() => handler.handleError(error, messageExternalId)).toThrow(
|
||||
expect.objectContaining({
|
||||
code: MessageImportDriverExceptionCode.TEMPORARY_ERROR,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle 401 Invalid Credentials', () => {
|
||||
const error = gmailBatchApiErrorMocks.getError(401);
|
||||
const exception = parseGmailApiBatchError(error, messageExternalId);
|
||||
const error = getGmailApiError({ code: 401 });
|
||||
|
||||
expect(exception).toBeInstanceOf(MessageImportDriverException);
|
||||
expect(exception?.code).toBe(
|
||||
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
|
||||
);
|
||||
expect(exception?.message).toBe(
|
||||
`${error.errors[0].message} for message with externalId: ${messageExternalId}`,
|
||||
expect(() => handler.handleError(error, messageExternalId)).toThrow(
|
||||
expect.objectContaining({
|
||||
code: MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle 403 Daily Limit Exceeded', () => {
|
||||
const error = gmailBatchApiErrorMocks.getError(403, 'dailyLimit');
|
||||
const exception = parseGmailApiBatchError(error, messageExternalId);
|
||||
const error = getGmailApiError({ code: 403, reason: 'dailyLimit' });
|
||||
|
||||
expect(exception).toBeInstanceOf(MessageImportDriverException);
|
||||
expect(exception?.code).toBe(
|
||||
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
|
||||
);
|
||||
expect(exception?.message).toBe(
|
||||
`${error.errors[0].message} for message with externalId: ${messageExternalId}`,
|
||||
expect(() => handler.handleError(error, messageExternalId)).toThrow(
|
||||
expect.objectContaining({
|
||||
code: MessageImportDriverExceptionCode.TEMPORARY_ERROR,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle 403 User Rate Limit Exceeded', () => {
|
||||
const error = gmailBatchApiErrorMocks.getError(403, 'userRateLimit');
|
||||
const exception = parseGmailApiBatchError(error, messageExternalId);
|
||||
const error = getGmailApiError({ code: 403, reason: 'userRateLimit' });
|
||||
|
||||
expect(exception).toBeInstanceOf(MessageImportDriverException);
|
||||
expect(exception?.code).toBe(
|
||||
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
|
||||
);
|
||||
expect(exception?.message).toBe(
|
||||
`${error.errors[0].message} for message with externalId: ${messageExternalId}`,
|
||||
expect(() => handler.handleError(error, messageExternalId)).toThrow(
|
||||
expect.objectContaining({
|
||||
code: MessageImportDriverExceptionCode.TEMPORARY_ERROR,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle 403 Rate Limit Exceeded', () => {
|
||||
const error = gmailBatchApiErrorMocks.getError(403, 'rateLimit');
|
||||
const exception = parseGmailApiBatchError(error, messageExternalId);
|
||||
const error = getGmailApiError({ code: 403, reason: 'rateLimit' });
|
||||
|
||||
expect(exception).toBeInstanceOf(MessageImportDriverException);
|
||||
expect(exception?.code).toBe(
|
||||
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
|
||||
);
|
||||
expect(exception?.message).toBe(
|
||||
`${error.errors[0].message} for message with externalId: ${messageExternalId}`,
|
||||
expect(() => handler.handleError(error, messageExternalId)).toThrow(
|
||||
expect.objectContaining({
|
||||
code: MessageImportDriverExceptionCode.TEMPORARY_ERROR,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle 403 Domain Policy Error', () => {
|
||||
const error = gmailBatchApiErrorMocks.getError(403, 'domainPolicy');
|
||||
const exception = parseGmailApiBatchError(error, messageExternalId);
|
||||
const error = getGmailApiError({ code: 403, reason: 'domainPolicy' });
|
||||
|
||||
expect(exception).toBeInstanceOf(MessageImportDriverException);
|
||||
expect(exception?.code).toBe(
|
||||
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
|
||||
);
|
||||
expect(exception?.message).toBe(
|
||||
`${error.errors[0].message} for message with externalId: ${messageExternalId}`,
|
||||
expect(() => handler.handleError(error, messageExternalId)).toThrow(
|
||||
expect.objectContaining({
|
||||
code: MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle 404 Not Found', () => {
|
||||
const error = gmailBatchApiErrorMocks.getError(404);
|
||||
const exception = parseGmailApiBatchError(error, messageExternalId);
|
||||
it('should handle 404 Not Found by returning silently', () => {
|
||||
const error = getGmailApiError({ code: 404 });
|
||||
|
||||
expect(exception).toBeUndefined();
|
||||
expect(() => handler.handleError(error, messageExternalId)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle 410 Gone', () => {
|
||||
const error = gmailBatchApiErrorMocks.getError(410);
|
||||
const exception = parseGmailApiBatchError(error, messageExternalId);
|
||||
it('should handle 410 Gone by returning silently', () => {
|
||||
const error = getGmailApiError({ code: 410 });
|
||||
|
||||
expect(exception).toBeUndefined();
|
||||
expect(() => handler.handleError(error, messageExternalId)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle 429 Too Many Requests', () => {
|
||||
const error = gmailBatchApiErrorMocks.getError(429, 'concurrent');
|
||||
const exception = parseGmailApiBatchError(error, messageExternalId);
|
||||
const error = getGmailApiError({ code: 429 });
|
||||
|
||||
expect(exception).toBeInstanceOf(MessageImportDriverException);
|
||||
expect(exception?.code).toBe(
|
||||
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
|
||||
);
|
||||
expect(exception?.message).toBe(
|
||||
`${error.errors[0].message} for message with externalId: ${messageExternalId}`,
|
||||
expect(() => handler.handleError(error, messageExternalId)).toThrow(
|
||||
expect.objectContaining({
|
||||
code: MessageImportDriverExceptionCode.TEMPORARY_ERROR,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle 500 Backend Error', () => {
|
||||
const error = gmailBatchApiErrorMocks.getError(500);
|
||||
const exception = parseGmailApiBatchError(error, messageExternalId);
|
||||
const error = getGmailApiError({ code: 500 });
|
||||
|
||||
expect(exception).toBeInstanceOf(MessageImportDriverException);
|
||||
expect(exception?.code).toBe(
|
||||
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
|
||||
);
|
||||
expect(exception?.message).toBe(
|
||||
`${error.errors[0].message} for message with externalId: ${messageExternalId}`,
|
||||
expect(() => handler.handleError(error, messageExternalId)).toThrow(
|
||||
expect.objectContaining({
|
||||
code: MessageImportDriverExceptionCode.TEMPORARY_ERROR,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
import { type MessageQuery } from 'src/modules/messaging/message-import-manager/types/message-or-thread-query';
|
||||
|
||||
export const createQueriesFromMessageIds = (
|
||||
messageExternalIds: string[],
|
||||
): MessageQuery[] => {
|
||||
return messageExternalIds.map((messageId) => ({
|
||||
uri: '/gmail/v1/users/me/messages/' + messageId + '?format=FULL',
|
||||
}));
|
||||
};
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
import { type GmailApiBatchError } from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-api-batch-error.type';
|
||||
|
||||
export const isGmailApiBatchError = (
|
||||
error: unknown,
|
||||
): error is GmailApiBatchError => {
|
||||
if (error === null || typeof error !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
!('code' in error) ||
|
||||
!('errors' in error) ||
|
||||
!Array.isArray(error.errors) ||
|
||||
error.errors.length === 0 ||
|
||||
error.errors.some((error) => !('reason' in error) || !('message' in error))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
-112
@@ -1,112 +0,0 @@
|
||||
import {
|
||||
MessageImportDriverException,
|
||||
MessageImportDriverExceptionCode,
|
||||
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
|
||||
import { type GmailApiBatchError } from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-api-batch-error.type';
|
||||
|
||||
export const parseGmailApiBatchError = (
|
||||
error: GmailApiBatchError,
|
||||
messageExternalId?: string,
|
||||
): MessageImportDriverException | undefined => {
|
||||
const { code, errors } = error;
|
||||
|
||||
const reason = errors?.[0]?.reason;
|
||||
const originalMessage = errors?.[0]?.message;
|
||||
const message = `${errors?.[0]?.message} for message with externalId: ${messageExternalId}`;
|
||||
|
||||
switch (code) {
|
||||
case 400:
|
||||
if (reason === 'invalid_grant') {
|
||||
return new MessageImportDriverException(
|
||||
message,
|
||||
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
|
||||
);
|
||||
}
|
||||
if (reason === 'failedPrecondition') {
|
||||
if (originalMessage.includes('Mail service not enabled')) {
|
||||
return new MessageImportDriverException(
|
||||
message,
|
||||
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
|
||||
);
|
||||
}
|
||||
|
||||
return new MessageImportDriverException(
|
||||
message,
|
||||
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
return new MessageImportDriverException(
|
||||
message,
|
||||
MessageImportDriverExceptionCode.UNKNOWN,
|
||||
);
|
||||
|
||||
case 404:
|
||||
case 410:
|
||||
return undefined;
|
||||
|
||||
case 429:
|
||||
return new MessageImportDriverException(
|
||||
message,
|
||||
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
|
||||
);
|
||||
|
||||
case 403:
|
||||
if (
|
||||
reason === 'rateLimitExceeded' ||
|
||||
reason === 'userRateLimitExceeded' ||
|
||||
reason === 'dailyLimitExceeded'
|
||||
) {
|
||||
return new MessageImportDriverException(
|
||||
message,
|
||||
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
|
||||
);
|
||||
}
|
||||
if (reason === 'domainPolicy') {
|
||||
return new MessageImportDriverException(
|
||||
message,
|
||||
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 401:
|
||||
return new MessageImportDriverException(
|
||||
message,
|
||||
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
|
||||
);
|
||||
|
||||
case 503:
|
||||
return new MessageImportDriverException(
|
||||
message,
|
||||
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
|
||||
);
|
||||
|
||||
case 500:
|
||||
case 502:
|
||||
case 504:
|
||||
if (reason === 'backendError') {
|
||||
return new MessageImportDriverException(
|
||||
message,
|
||||
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
if (errors?.[0]?.message.includes(`Authentication backend unavailable`)) {
|
||||
return new MessageImportDriverException(
|
||||
`${code} - ${reason} - ${message}`,
|
||||
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return new MessageImportDriverException(
|
||||
message,
|
||||
MessageImportDriverExceptionCode.UNKNOWN,
|
||||
);
|
||||
};
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
type Query = {
|
||||
uri: string;
|
||||
};
|
||||
|
||||
export type BatchQueries = Query[];
|
||||
Reference in New Issue
Block a user