diff --git a/connectors/src/connectors/zendesk/lib/errors.ts b/connectors/src/connectors/zendesk/lib/errors.ts new file mode 100644 index 000000000000..03b6a9d5062e --- /dev/null +++ b/connectors/src/connectors/zendesk/lib/errors.ts @@ -0,0 +1,19 @@ +/** + * Errors returned by the library node-zendesk. + * Check out https://github.com/blakmatrix/node-zendesk/blob/fa069d927bd418ee2058bb7bb913f9414e395110/src/clients/helpers.js#L262 + */ +interface NodeZendeskError extends Error { + statusCode: number; + result: string | null; +} + +export function isNodeZendeskForbiddenError( + err: unknown +): err is NodeZendeskError { + return ( + typeof err === "object" && + err !== null && + "statusCode" in err && + err.statusCode === 403 + ); +} diff --git a/connectors/src/connectors/zendesk/lib/zendesk_api.ts b/connectors/src/connectors/zendesk/lib/zendesk_api.ts index 0b725332025e..2c39c4f63663 100644 --- a/connectors/src/connectors/zendesk/lib/zendesk_api.ts +++ b/connectors/src/connectors/zendesk/lib/zendesk_api.ts @@ -5,9 +5,11 @@ import { createClient } from "node-zendesk"; import type { ZendeskFetchedArticle, ZendeskFetchedCategory, + ZendeskFetchedSection, ZendeskFetchedTicket, ZendeskFetchedUser, } from "@connectors/@types/node-zendesk"; +import { isNodeZendeskForbiddenError } from "@connectors/connectors/zendesk/lib/errors"; import { ExternalOAuthTokenError } from "@connectors/lib/error"; import logger from "@connectors/logger/logger"; import type { ZendeskCategoryResource } from "@connectors/resources/zendesk_resources"; @@ -147,6 +149,8 @@ async function fetchFromZendeskWithRetries({ `[Zendesk] Zendesk API 404 error on: ${getEndpointFromUrl(url)}` ); return null; + } else if (rawResponse.status === 403) { + throw new ExternalOAuthTokenError(); } logger.error( { rawResponse, status: rawResponse.status, text: rawResponse.text }, @@ -361,3 +365,30 @@ export async function fetchZendeskCurrentUser({ const data = await response.json(); return data.user; } + +/** + * Fetches the Section and the User for an article. + */ +export async function fetchArticleMetadata( + zendeskApiClient: Client, + article: ZendeskFetchedArticle +): Promise<{ section: ZendeskFetchedSection; user: ZendeskFetchedUser }> { + try { + const { result: section } = await zendeskApiClient.helpcenter.sections.show( + article.section_id + ); + const { result: user } = await zendeskApiClient.users.show( + article.author_id + ); + return { section, user }; + } catch (e) { + logger.error( + { articleId: article.id, error: e }, + "[Zendesk] Error fetching article metadata" + ); + if (isNodeZendeskForbiddenError(e)) { + throw new ExternalOAuthTokenError(e); + } + throw e; + } +} diff --git a/connectors/src/connectors/zendesk/temporal/activities.ts b/connectors/src/connectors/zendesk/temporal/activities.ts index 6106ecc6e686..07afe6c274c4 100644 --- a/connectors/src/connectors/zendesk/temporal/activities.ts +++ b/connectors/src/connectors/zendesk/temporal/activities.ts @@ -1,5 +1,6 @@ import type { ModelId } from "@dust-tt/types"; +import { isNodeZendeskForbiddenError } from "@connectors/connectors/zendesk/lib/errors"; import { syncArticle } from "@connectors/connectors/zendesk/lib/sync_article"; import { syncCategory } from "@connectors/connectors/zendesk/lib/sync_category"; import { syncTicket } from "@connectors/connectors/zendesk/lib/sync_ticket"; @@ -14,6 +15,7 @@ import { import { ZENDESK_BATCH_SIZE } from "@connectors/connectors/zendesk/temporal/config"; import { dataSourceConfigFromConnector } from "@connectors/lib/api/data_source_config"; import { concurrentExecutor } from "@connectors/lib/async_utils"; +import { ExternalOAuthTokenError } from "@connectors/lib/error"; import { ZendeskTimestampCursor } from "@connectors/lib/models/zendesk"; import { syncStarted, syncSucceeded } from "@connectors/lib/sync_status"; import logger from "@connectors/logger/logger"; @@ -328,11 +330,21 @@ export async function syncZendeskArticleBatchActivity({ `[Zendesk] Processing ${articles.length} articles in batch` ); - const sections = - await zendeskApiClient.helpcenter.sections.listByCategory(categoryId); - const { result: users } = await zendeskApiClient.users.showMany( - articles.map((article) => article.author_id) - ); + let sections; + let users; + try { + sections = + await zendeskApiClient.helpcenter.sections.listByCategory(categoryId); + const { result: usersResult } = await zendeskApiClient.users.showMany( + articles.map((article) => article.author_id) + ); + users = usersResult; + } catch (e) { + if (isNodeZendeskForbiddenError(e)) { + throw new ExternalOAuthTokenError(e); + } + throw e; + } await concurrentExecutor( articles, diff --git a/connectors/src/connectors/zendesk/temporal/incremental_activities.ts b/connectors/src/connectors/zendesk/temporal/incremental_activities.ts index de37de067f33..ad9df74a4a28 100644 --- a/connectors/src/connectors/zendesk/temporal/incremental_activities.ts +++ b/connectors/src/connectors/zendesk/temporal/incremental_activities.ts @@ -9,6 +9,7 @@ import { getZendeskSubdomainAndAccessToken } from "@connectors/connectors/zendes import { changeZendeskClientSubdomain, createZendeskClient, + fetchArticleMetadata, fetchRecentlyUpdatedArticles, fetchRecentlyUpdatedTickets, } from "@connectors/connectors/zendesk/lib/zendesk_api"; @@ -112,10 +113,9 @@ export async function syncZendeskArticleUpdateBatchActivity({ await concurrentExecutor( articles, async (article) => { - const { result: section } = - await zendeskApiClient.helpcenter.sections.show(article.section_id); - const { result: user } = await zendeskApiClient.users.show( - article.author_id + const { section, user } = await fetchArticleMetadata( + zendeskApiClient, + article ); if (section.category_id) {