diff --git a/packages/integration-client/src/index.ts b/packages/integration-client/src/index.ts index 9ebaea02a9..a64162786d 100644 --- a/packages/integration-client/src/index.ts +++ b/packages/integration-client/src/index.ts @@ -17,3 +17,4 @@ export * from './client' export * from './types' export * from './utils' export * from './events' +export * from './request' diff --git a/packages/integration-client/src/request.ts b/packages/integration-client/src/request.ts new file mode 100644 index 0000000000..06c2d30685 --- /dev/null +++ b/packages/integration-client/src/request.ts @@ -0,0 +1,94 @@ +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. + +import { concatLink } from '@hcengineering/core' +import platform, { PlatformError, Status, Severity } from '@hcengineering/platform' + +/** + * Options for making HTTP requests to integration service APIs. + */ +export interface RequestOptions { + /** The base URL of the API endpoint */ + baseUrl: string + /** HTTP method to use */ + method: 'GET' | 'POST' | 'DELETE' + /** Optional path to append to the base URL */ + path?: string + /** Optional Bearer token for authorization */ + token?: string + /** Optional request body (will be JSON stringified) */ + body?: any +} + +/** + * Makes HTTP requests to integration service APIs with proper error handling. + * + * @param options - Request configuration options + * @returns Promise that resolves to the parsed JSON response, or undefined for empty responses + * @throws {PlatformError} When network errors occur or HTTP status indicates failure + */ +export async function request (options: RequestOptions): Promise { + const { baseUrl, method, path, token, body } = options + let response: Response + try { + response = await fetch(concatLink(baseUrl, path ?? ''), { + method, + headers: { + ...(token !== undefined ? { Authorization: 'Bearer ' + token } : {}), + 'Content-Type': 'application/json' + }, + ...(body !== undefined ? { body: JSON.stringify(body) } : {}) + }) + } catch (err) { + throw new PlatformError( + new Status(Severity.ERROR, platform.status.ConnectionClosed, { + message: 'Network error occurred' + }) + ) + } + + if (response.status === 200) { + const contentLength = response.headers.get('content-length') + const contentType = response.headers.get('content-type') ?? '' + + if (contentLength === '0' || (!contentType.includes('application/json') && !contentType.includes('text/json'))) { + return undefined + } + + const text = await response.text() + if (text.trim() === '') { + return undefined + } + + try { + return JSON.parse(text) + } catch (error) { + console.warn('Failed to parse JSON response:', text, error) + return undefined + } + } else if (response.status === 202) { + return undefined + } else if (response.status === 401) { + throw new PlatformError(new Status(Severity.ERROR, platform.status.Unauthorized, {})) + } else if (response.status === 403) { + throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {})) + } else if (response.status === 404) { + throw new PlatformError( + new Status(Severity.ERROR, platform.status.ResourceNotFound, { resource: options.path ?? '' }) + ) + } else if (response.status >= 500) { + throw new PlatformError(new Status(Severity.ERROR, platform.status.InternalServerError, {})) + } else { + throw new PlatformError(new Status(Severity.ERROR, platform.status.BadRequest, { status: response.status })) + } +} diff --git a/plugins/calendar-resources/src/api.ts b/plugins/calendar-resources/src/api.ts index 87467ab129..6b051453d9 100644 --- a/plugins/calendar-resources/src/api.ts +++ b/plugins/calendar-resources/src/api.ts @@ -13,14 +13,14 @@ // limitations under the License. // -import { concatLink } from '@hcengineering/core' import { getMetadata } from '@hcengineering/platform' import presentation from '@hcengineering/presentation' import login from '@hcengineering/login' import { type Integration } from '@hcengineering/account-client' import { type IntegrationClient, - getIntegrationClient as getIntegrationClientRaw + getIntegrationClient as getIntegrationClientRaw, + request } from '@hcengineering/integration-client' import calendar from './plugin' import { calendarIntegrationKind } from '@hcengineering/calendar' @@ -36,12 +36,11 @@ export async function signout (integration: Integration, client: IntegrationClie if (email === undefined) { throw new Error('Email is not defined in integration') } - await fetch(concatLink(url, `/signout?value=${email}`), { + await request({ + baseUrl: url, + path: `/signout?value=${email}`, method: 'GET', - headers: { - Authorization: 'Bearer ' + token, - 'Content-Type': 'application/json' - } + token }) } diff --git a/plugins/gmail-resources/src/api.ts b/plugins/gmail-resources/src/api.ts index ed90d8e7b0..2b197cd17d 100644 --- a/plugins/gmail-resources/src/api.ts +++ b/plugins/gmail-resources/src/api.ts @@ -18,11 +18,11 @@ import login from '@hcengineering/login' import { type GmailSyncState, gmailIntegrationKind } from '@hcengineering/gmail' import { getIntegrationClient as getIntegrationClientRaw, - type IntegrationClient + type IntegrationClient, + request as httpRequest } from '@hcengineering/integration-client' import gmail from './plugin' -import { concatLink } from '@hcengineering/core' export async function getIntegrationClient (): Promise { const accountsUrl = getMetadata(login.metadata.AccountsUrl) @@ -36,22 +36,13 @@ export async function getIntegrationClient (): Promise { const url = getMetadata(gmail.metadata.GmailURL) ?? '' async function request (method: 'GET' | 'POST' | 'DELETE', path?: string, body?: any): Promise { - const response = await fetch(concatLink(url, path ?? ''), { + return await httpRequest({ + baseUrl: url, method, - headers: { - Authorization: 'Bearer ' + getMetadata(presentation.metadata.Token), - 'Content-Type': 'application/json' - }, - ...(body !== undefined ? { body: JSON.stringify(body) } : {}) + path, + token: getMetadata(presentation.metadata.Token), + body }) - - if (response.status === 200) { - return await response.json() - } else if (response.status === 202) { - return undefined - } else { - throw new Error(`Unexpected response: ${response.status}`) - } } export async function getState (socialId: string): Promise { diff --git a/plugins/setting-resources/src/components/integrations/Integrations.svelte b/plugins/setting-resources/src/components/integrations/Integrations.svelte index 88ba89df2b..c8a94e4f64 100644 --- a/plugins/setting-resources/src/components/integrations/Integrations.svelte +++ b/plugins/setting-resources/src/components/integrations/Integrations.svelte @@ -117,7 +117,6 @@ } function onRefreshIntegrations (data: any): void { - console.log('Refreshing integrations due to:', data.integrationKind, data.operation) lastEventTime = Date.now() void refreshIntegrations() } @@ -229,7 +228,6 @@ } return integrationInfo }) - console.log('Filtered Integrations:', filteredIntegrations) return filteredIntegrations } diff --git a/plugins/telegram-resources/src/api.ts b/plugins/telegram-resources/src/api.ts index b5a06db899..bf01d12127 100644 --- a/plugins/telegram-resources/src/api.ts +++ b/plugins/telegram-resources/src/api.ts @@ -13,14 +13,15 @@ // import { concatLink, type PersonId } from '@hcengineering/core' -import platform, { getMetadata, PlatformError, Status, Severity } from '@hcengineering/platform' +import { getMetadata } from '@hcengineering/platform' import telegram from './plugin' import presentation, { getCurrentWorkspaceUuid } from '@hcengineering/presentation' import login from '@hcengineering/login' import { telegramIntegrationKind } from '@hcengineering/telegram' import { getIntegrationClient as getIntegrationClientRaw, - type IntegrationClient + type IntegrationClient, + request as httpRequest } from '@hcengineering/integration-client' import { withRetry } from '@hcengineering/retry' import type { Integration } from '@hcengineering/account-client' @@ -50,55 +51,11 @@ export interface TelegramChannelData { } const url = getMetadata(telegram.metadata.TelegramURL) ?? '' - -async function _request (method: 'GET' | 'POST' | 'DELETE', path?: string, body?: any): Promise { - const base = concatLink(url, 'api/integrations') - - let response: Response - try { - response = await fetch(concatLink(base, path ?? ''), { - method, - headers: { - Authorization: 'Bearer ' + getMetadata(presentation.metadata.Token), - 'Content-Type': 'application/json' - }, - ...(body !== undefined ? { body: JSON.stringify(body) } : {}) - }) - } catch (err) { - throw new PlatformError( - new Status(Severity.ERROR, platform.status.ConnectionClosed, { - message: 'Network error occurred' - }) - ) - } - - if (response.status === 200) { - try { - return await response.json() - } catch (err) { - throw new PlatformError( - new Status(Severity.ERROR, platform.status.BadRequest, { - message: 'Failed to parse response JSON' - }) - ) - } - } else if (response.status === 202) { - return undefined - } else if (response.status === 401) { - throw new PlatformError(new Status(Severity.ERROR, platform.status.Unauthorized, {})) - } else if (response.status === 403) { - throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {})) - } else if (response.status === 404) { - throw new PlatformError(new Status(Severity.ERROR, platform.status.ResourceNotFound, { resource: path ?? '' })) - } else if (response.status >= 500) { - throw new PlatformError(new Status(Severity.ERROR, platform.status.InternalServerError, {})) - } else { - throw new PlatformError(new Status(Severity.ERROR, platform.status.BadRequest, { status: response.status })) - } -} +const baseUrl = concatLink(url, 'api/integrations') async function request (method: 'GET' | 'POST' | 'DELETE', path?: string, body?: any): Promise { - return await withRetry(async () => await _request(method, path, body)) + const token = getMetadata(presentation.metadata.Token) + return await withRetry(async () => await httpRequest({ baseUrl, method, path, token, body })) } export async function getState (phone: string): Promise {