diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 1a06373ad0b..44a202a7610 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -424,6 +424,9 @@ importers: '@rush-temp/huly-mail-resources': specifier: file:./projects/huly-mail-resources.tgz version: file:projects/huly-mail-resources.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)))(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) + '@rush-temp/hulylake': + specifier: file:./projects/hulylake.tgz + version: file:projects/hulylake.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9) '@rush-temp/hulylake-client': specifier: file:./projects/hulylake-client.tgz version: file:projects/hulylake-client.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) @@ -2180,7 +2183,7 @@ importers: specifier: ^0.4.2 version: 0.4.2(prosemirror-inputrules@1.4.0)(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.2) puppeteer: - specifier: ^24.17.1 + specifier: ^24.23.0 version: 24.23.0(bufferutil@4.0.8)(typescript@5.8.3)(utf-8-validate@6.0.4) qs: specifier: ~6.11.0 @@ -4786,6 +4789,10 @@ packages: resolution: {integrity: sha512-0sAr+WkQpteDpwkXdeGMiC4VhoOU5uTVJqUE4m/mpmCQcTRzjZJwB+H0Rwwsd+NRWqEgsp8mu5uuEA8W0lkXfw==, tarball: file:projects/hulylake-client.tgz} version: 0.0.0 + '@rush-temp/hulylake@file:projects/hulylake.tgz': + resolution: {integrity: sha512-J7ciORiWjz48KhhqND5qSyZK07hYUGkiusSRkYuUAAKVR5fijYfahll840aUmEEynJNUVM8c77Du2mG8gMCeTg==, tarball: file:projects/hulylake.tgz} + version: 0.0.0 + '@rush-temp/hulypulse-client@file:projects/hulypulse-client.tgz': resolution: {integrity: sha512-5sJHgbSOB2n85z/ANFxNhqppxQO4N6CsaTyDDCZdwkqRQOSrw+lTj2ERBjwthY+QEXitsgaP3Or6Kcwnac/5Ow==, tarball: file:projects/hulypulse-client.tgz} version: 0.0.0 @@ -5819,7 +5826,7 @@ packages: version: 0.0.0 '@rush-temp/server-storage@file:projects/server-storage.tgz': - resolution: {integrity: sha512-d5VV5hAFam8w5w03CoMothLBvQ3EnKfKzaoPQ0JJ/+wT+xkydGm08KsJSeSLJSYyXWJ0Ikoh6l/4o+Ii8wSQOw==, tarball: file:projects/server-storage.tgz} + resolution: {integrity: sha512-Tu1t/olfCXWByKhVUgklFIkz6qUsu1d4AgP37Ylf8Fe1yZ7yBZbEq6Se43ZtQxIET7fUFYHIP9VFZZqQPCjoQg==, tarball: file:projects/server-storage.tgz} version: 0.0.0 '@rush-temp/server-tags-resources@file:projects/server-tags-resources.tgz': @@ -20980,6 +20987,33 @@ snapshots: - supports-color - ts-node + '@rush-temp/hulylake@file:projects/hulylake.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@swc/core@1.13.5)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)': + dependencies: + '@types/jest': 29.5.12 + '@types/node': 22.15.29 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3))(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint-plugin-n@15.7.0(eslint@8.56.0))(eslint-plugin-promise@6.1.1(eslint@8.56.0))(eslint@8.56.0)(typescript@5.8.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + jest: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)) + prettier: 3.2.5 + ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.25.9)(jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3)))(typescript@5.8.3) + ts-node: 10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - '@babel/core' + - '@jest/types' + - '@swc/core' + - '@swc/wasm' + - babel-jest + - babel-plugin-macros + - esbuild + - node-notifier + - supports-color + '@rush-temp/hulypulse-client@file:projects/hulypulse-client.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(encoding@0.1.13)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.15.29)(typescript@5.8.3))': dependencies: '@types/jest': 29.5.12 diff --git a/communication b/communication index 047325b57fd..b8a9ca5625f 160000 --- a/communication +++ b/communication @@ -1 +1 @@ -Subproject commit 047325b57fd86be9ffce343ec499e72289c29f47 +Subproject commit b8a9ca5625f46ac7dbffe63cc1c795592c106762 diff --git a/dev/prod/public/config-dev.json b/dev/prod/public/config-dev.json index d8e00391634..38e66f03420 100644 --- a/dev/prod/public/config-dev.json +++ b/dev/prod/public/config-dev.json @@ -12,6 +12,7 @@ "PUBLIC_SCHEDULE_URL": "https://schedule.hc.engineering", "CALDAV_SERVER_URL": "https://caldav.hc.engineering", "BACKUP_URL": "https://front.hc.engineering/api/backup", + "HULYLAKE_URL": "https://lake.hc.engineering", "PULSE_URL": "wss://pulse.hc.engineering/ws", "COMMUNICATION_API_ENABLED": "true", "FILES_URL": "https://datalake.hc.engineering/blob/:workspace/:blobId/:filename" diff --git a/dev/tool/src/communication.ts b/dev/tool/src/communication.ts index 312095a7e76..9b414ad4c0e 100644 --- a/dev/tool/src/communication.ts +++ b/dev/tool/src/communication.ts @@ -12,7 +12,7 @@ // limitations under the License. import { type Workspace } from '@hcengineering/account' -import { type HulylakeClient, type JsonPatch } from '@hcengineering/hulylake-client' +import { type HulylakeWorkspaceClient, type JsonPatch } from '@hcengineering/hulylake-client' import type postgres from 'postgres' import { generateUuid, @@ -60,7 +60,7 @@ export async function migrateWorkspaceMessages ( ws: Workspace, card: CardID | undefined, db: postgres.Sql, - hulylake: HulylakeClient, + hulylake: HulylakeWorkspaceClient, accountClient: AccountClient, personUuidBySocialId: Map ): Promise { @@ -72,7 +72,7 @@ async function migrateMessages ( ws: Workspace, card: CardID | undefined, db: postgres.Sql, - hulylake: HulylakeClient, + hulylake: HulylakeWorkspaceClient, accountClient: AccountClient, personUuidBySocialId: Map ): Promise { @@ -105,7 +105,7 @@ async function migrateMessages ( async function migrateMessagesBatch ( ctx: MeasureContext, cardId: CardID, - hulylake: HulylakeClient, + hulylake: HulylakeWorkspaceClient, accountClient: AccountClient, personUuidBySocialId: Map, messages: OldMessage[] @@ -161,7 +161,11 @@ async function migrateMessagesBatch ( } } -async function getGroups (ctx: MeasureContext, hulylake: HulylakeClient, cardId: CardID): Promise { +async function getGroups ( + ctx: MeasureContext, + hulylake: HulylakeWorkspaceClient, + cardId: CardID +): Promise { const res = await hulylake.getJson(`${cardId}/messages/groups`, { maxRetries: 3, isRetryable: () => true, diff --git a/dev/tool/src/index.ts b/dev/tool/src/index.ts index 4e88e4a7e49..be29745acf9 100644 --- a/dev/tool/src/index.ts +++ b/dev/tool/src/index.ts @@ -56,7 +56,7 @@ import { type Account as OldAccount, type Workspace as OldWorkspace } from '@hcengineering/account-service' -import { getClient as getHulylakeClient } from '@hcengineering/hulylake-client' +import { getWorkspaceClient as getHulylakeClient } from '@hcengineering/hulylake-client' import { getDBClient, createPostgreeDestroyAdapter, diff --git a/packages/hulylake-client/src/client.ts b/packages/hulylake-client/src/client.ts index 956a8f6d231..08a431e7284 100644 --- a/packages/hulylake-client/src/client.ts +++ b/packages/hulylake-client/src/client.ts @@ -17,25 +17,91 @@ import { WorkspaceUuid } from '@hcengineering/core' import { RetryOptions } from '@hcengineering/retry' import { fetchSafe, unwrapContentLength, unwrapEtag, unwrapLastModified } from './utils' -import { HulyHeaders, HulylakeClient, HulyMeta, HulyResponse, JsonPatch, PatchOptions, PutOptions, Body } from './types' +import { + HulyHeaders, + HulylakeClient, + HulylakeWorkspaceClient, + HulyMeta, + HulyResponse, + JsonPatch, + PatchOptions, + PutOptions, + Body +} from './types' + +export function getWorkspaceClient (baseUrl: string, workspace: WorkspaceUuid, token: string): HulylakeWorkspaceClient { + const client = new Client(baseUrl, token) + return new WorkspaceClient(client, workspace) +} + +export function getClient (baseUrl: string, token: string): HulylakeClient { + return new Client(baseUrl, token) +} + +class WorkspaceClient implements HulylakeWorkspaceClient { + constructor ( + private readonly client: HulylakeClient, + private readonly workspace: WorkspaceUuid + ) {} -export function getClient (baseUrl: string, workspace: WorkspaceUuid, token: string): HulylakeClient { - return new Client(baseUrl, workspace, token) + head (key: string, retryOptions?: RetryOptions): Promise> { + return this.client.head(this.workspace, key, retryOptions) + } + + get (key: string, retryOptions?: RetryOptions): Promise>> { + return this.client.get(this.workspace, key, retryOptions) + } + + put (key: string, body: Body, opts: PutOptions, retryOptions?: RetryOptions): Promise> { + return this.client.put(this.workspace, key, body, opts, retryOptions) + } + + patch (key: string, body: Body, opts: PatchOptions, retryOptions?: RetryOptions): Promise> { + return this.client.patch(this.workspace, key, body, opts, retryOptions) + } + + delete (key: string, retryOptions?: RetryOptions): Promise> { + return this.client.delete(this.workspace, key, retryOptions) + } + + public async getJson(key: string, retryOptions?: RetryOptions): Promise> { + const res = await this.client.get(this.workspace, key, retryOptions) + const body = res.ok && res.body != null ? ((await new Response(res.body).json()) as T) : undefined + return { ...res, body } + } + + public async putJson( + key: string, + json: T, + options?: Omit, + retryOptions?: RetryOptions + ): Promise> { + return await this.put(key, JSON.stringify(json), { ...options, mergeStrategy: 'jsonpatch' }, retryOptions) + } + + public async patchJson ( + key: string, + body: JsonPatch[], + options?: Omit, + retryOptions?: RetryOptions + ): Promise> { + return await this.patch( + key, + JSON.stringify(body), + { ...options, contentType: 'application/json-patch+json' }, + retryOptions + ) + } } class Client implements HulylakeClient { constructor ( private readonly baseUrl: string, - private readonly workspace: WorkspaceUuid, private readonly token: string ) { this.baseUrl = this.baseUrl.endsWith('/') ? this.baseUrl.slice(0, -1) : this.baseUrl } - private objectUrl (key: string): string { - return `${this.baseUrl}/api/${this.workspace}/${encodeURIComponent(key)}` - } - private authHeaders (init?: HeadersInit): Headers { const headers = new Headers(init) headers.set('Authorization', `Bearer ${this.token}`) @@ -59,9 +125,13 @@ class Client implements HulylakeClient { } } - public async head (key: string, retryOptions?: RetryOptions): Promise> { + public objectUrl (workspace: string, key: string): string { + return `${this.baseUrl}/api/${workspace}/${encodeURIComponent(key)}` + } + + public async head (workspace: string, key: string, retryOptions?: RetryOptions): Promise> { const res = await fetchSafe( - this.objectUrl(key), + this.objectUrl(workspace, key), { method: 'HEAD', headers: this.authHeaders() @@ -74,15 +144,20 @@ class Client implements HulylakeClient { status: res.status, etag: unwrapEtag(res.headers.get('ETag')), lastModified: unwrapLastModified(res.headers.get('Last-Modified')), + contentType: res.headers.get('Content-Type') ?? 'application/octet-stream', contentLength: unwrapContentLength(res.headers.get('Content-Length')), headers: res.headers } } - public async get (key: string, retryOptions?: RetryOptions): Promise>> { + public async get ( + workspace: string, + key: string, + retryOptions?: RetryOptions + ): Promise>> { try { const res = await fetchSafe( - this.objectUrl(key), + this.objectUrl(workspace, key), { method: 'GET', headers: this.authHeaders() @@ -90,18 +165,55 @@ class Client implements HulylakeClient { retryOptions ) - let body: ReadableStream | undefined - - if (res.ok) { - body = res.body ?? undefined + return { + ok: res.ok, + status: res.status, + etag: unwrapEtag(res.headers.get('ETag')), + headers: res.headers, + lastModified: unwrapLastModified(res.headers.get('Last-Modified')), + contentType: res.headers.get('Content-Type') ?? 'application/octet-stream', + contentLength: unwrapContentLength(res.headers.get('Content-Length')), + body: res.ok ? res.body ?? undefined : undefined } + } catch (err: any) { + if (err.name === 'NotFoundError') { + return { + ok: false, + status: 404, + etag: undefined, + headers: new Headers(), + body: undefined + } + } + throw err + } + } + + public async partial ( + workspace: string, + key: string, + offset: number, + length?: number, + retryOptions?: RetryOptions + ): Promise>> { + try { + const res = await fetchSafe( + this.objectUrl(workspace, key), + { + method: 'GET', + headers: this.authHeaders({ + Range: length !== undefined ? `bytes=${offset}-${offset + length - 1}` : `bytes=${offset}` + }) + }, + retryOptions + ) return { ok: res.ok, status: res.status, etag: unwrapEtag(res.headers.get('ETag')), headers: res.headers, - body + body: res.ok ? res.body ?? undefined : undefined } } catch (err: any) { if (err.name === 'NotFoundError') { @@ -118,6 +230,7 @@ class Client implements HulylakeClient { } public async put ( + workspace: string, key: string, body: Body, opts: PutOptions = {}, @@ -125,6 +238,7 @@ class Client implements HulylakeClient { ): Promise> { const { mergeStrategy, headers, meta } = opts const contentType = 'contentType' in opts ? opts.contentType : undefined + const contentLength = 'contentLength' in opts ? opts.contentLength : undefined const h = this.authHeaders() @@ -138,15 +252,23 @@ class Client implements HulylakeClient { h.set('Content-Type', 'application/json') } + if (contentLength != null) { + h.set('Content-Length', contentLength.toString()) + } + this.applyHeaders(h, headers) this.applyMeta(h, meta) + const duplex = body instanceof ReadableStream ? 'half' : undefined + const res = await fetchSafe( - this.objectUrl(key), + this.objectUrl(workspace, key), { method: 'PUT', headers: h, - body: body as any + body, + // @ts-expect-error must present for ReadableStream but it is not in the interface + duplex }, retryOptions ) @@ -162,12 +284,13 @@ class Client implements HulylakeClient { } public async patch ( + workspace: string, key: string, body: Body, opts: PatchOptions = {}, retryOptions?: RetryOptions ): Promise> { - const { contentType, headers, meta } = opts + const { contentType, contentLength, headers, meta } = opts const h = this.authHeaders() @@ -175,15 +298,23 @@ class Client implements HulylakeClient { h.set('Content-Type', contentType) } + if (contentLength != null) { + h.set('Content-Length', contentLength.toString()) + } + this.applyHeaders(h, headers) this.applyMeta(h, meta) + const duplex = body instanceof ReadableStream ? 'half' : undefined + const res = await fetchSafe( - this.objectUrl(key), + this.objectUrl(workspace, key), { method: 'PATCH', headers: h, - body: body as any + body, + // @ts-expect-error must present for ReadableStream but it is not in the interface + duplex }, retryOptions ) @@ -194,70 +325,25 @@ class Client implements HulylakeClient { etag: unwrapEtag(res.headers.get('ETag')), lastModified: unwrapLastModified(res.headers.get('Last-Modified')), contentLength: unwrapContentLength(res.headers.get('Content-Length')), + contentType: res.headers.get('Content-Type') ?? undefined, headers: res.headers } } - public async getJson(key: string, retryOptions?: RetryOptions): Promise> { - try { - const res = await fetchSafe( - this.objectUrl(key), - { - method: 'GET', - headers: this.authHeaders() - }, - retryOptions - ) - - let body: T | undefined - - if (res.ok) { - body = (await res.json()) as T - } - - return { - ok: res.ok, - status: res.status, - etag: unwrapEtag(res.headers.get('ETag')), - lastModified: unwrapLastModified(res.headers.get('Last-Modified')), - contentLength: unwrapContentLength(res.headers.get('Content-Length')), - headers: res.headers, - body - } - } catch (err: any) { - if (err.name === 'NotFoundError') { - return { - ok: false, - status: 404, - etag: undefined, - headers: new Headers(), - body: undefined - } - } - throw err - } - } - - public async putJson( - key: string, - json: T, - options?: Omit, - retryOptions?: RetryOptions - ): Promise> { - return await this.put(key, JSON.stringify(json), { ...options, mergeStrategy: 'jsonpatch' }, retryOptions) - } - - public async patchJson ( - key: string, - body: JsonPatch[], - options?: Omit, - retryOptions?: RetryOptions - ): Promise> { - return await this.patch( - key, - JSON.stringify(body), - { ...options, contentType: 'application/json-patch+json' }, + public async delete (workspace: string, key: string, retryOptions?: RetryOptions): Promise> { + const res = await fetchSafe( + this.objectUrl(workspace, key), + { + method: 'DELETE', + headers: this.authHeaders() + }, retryOptions ) + + return { + ok: res.ok, + status: res.status, + headers: res.headers + } } } diff --git a/packages/hulylake-client/src/types.ts b/packages/hulylake-client/src/types.ts index bce6daf6f08..c9c53eb0d6f 100644 --- a/packages/hulylake-client/src/types.ts +++ b/packages/hulylake-client/src/types.ts @@ -16,10 +16,44 @@ import { RetryOptions } from '@hcengineering/retry' export interface HulylakeClient { + head: (workspace: string, key: string, retryOptions?: RetryOptions) => Promise> + get: ( + workspace: string, + key: string, + retryOptions?: RetryOptions + ) => Promise>> + partial: ( + workspace: string, + key: string, + offset: number, + length?: number, + retryOptions?: RetryOptions + ) => Promise>> + put: ( + workspace: string, + key: string, + body: Body, + opts: PutOptions, + retryOptions?: RetryOptions + ) => Promise> + patch: ( + workspace: string, + key: string, + body: Body, + opts: PatchOptions, + retryOptions?: RetryOptions + ) => Promise> + delete: (workspace: string, key: string, retryOptions?: RetryOptions) => Promise> + + objectUrl: (workspace: string, key: string) => string +} + +export interface HulylakeWorkspaceClient { head: (key: string, retryOptions?: RetryOptions) => Promise> get: (key: string, retryOptions?: RetryOptions) => Promise>> put: (key: string, body: Body, opts: PutOptions, retryOptions?: RetryOptions) => Promise> patch: (key: string, body: Body, opts: PatchOptions, retryOptions?: RetryOptions) => Promise> + delete: (key: string, retryOptions?: RetryOptions) => Promise> getJson: (key: string, retryOptions?: RetryOptions) => Promise> putJson: ( @@ -36,7 +70,7 @@ export interface HulylakeClient { ) => Promise> } -export type Body = ArrayBuffer | Blob | string +export type Body = ReadableStream | ArrayBuffer | Blob | string export type MergeStrategy = 'concatenate' | 'jsonpatch' export type HulyHeaders = Record export type HulyMeta = Record @@ -44,17 +78,20 @@ export type HulyMeta = Record export type PutOptions = | { mergeStrategy?: 'concatenate' + contentLength?: number contentType?: string headers?: HulyHeaders meta?: HulyMeta } | { mergeStrategy: 'jsonpatch' + contentLength?: number headers?: HulyHeaders meta?: HulyMeta } export interface PatchOptions { + contentLength?: number contentType?: string headers?: HulyHeaders meta?: HulyMeta @@ -75,6 +112,7 @@ export interface HulyResponse { ok: boolean status: number etag?: string + contentType?: string contentLength?: number lastModified?: number headers: Headers diff --git a/packages/hulylake-client/src/utils.ts b/packages/hulylake-client/src/utils.ts index 5202756d516..1d93750e867 100644 --- a/packages/hulylake-client/src/utils.ts +++ b/packages/hulylake-client/src/utils.ts @@ -29,12 +29,12 @@ async function innerFetchSafe (url: string | URL, init?: RequestInit): Promise { diff --git a/packages/presentation/src/communication.ts b/packages/presentation/src/communication.ts index d60ad8f6c5c..fd77c54a858 100644 --- a/packages/presentation/src/communication.ts +++ b/packages/presentation/src/communication.ts @@ -81,7 +81,7 @@ import { addNotification, NotificationSeverity, languageStore } from '@hcenginee import { getMetadata, translate } from '@hcengineering/platform' import view from '@hcengineering/view' import { get } from 'svelte/store' -import { getClient as getHulylakeClient } from '@hcengineering/hulylake-client' +import { getWorkspaceClient as getHulylakeClient } from '@hcengineering/hulylake-client' import { v4 as uuid } from 'uuid' import { getCurrentWorkspaceUuid } from './file' diff --git a/pods/external/services.d/hulylake.service b/pods/external/services.d/hulylake.service index db55d0400c0..990ce557dff 100644 --- a/pods/external/services.d/hulylake.service +++ b/pods/external/services.d/hulylake.service @@ -1 +1 @@ -hulylake hardcoreeng/service_hulylake:0.1.12 +hulylake hardcoreeng/service_hulylake:0.1.13 diff --git a/pods/fulltext/src/manager.ts b/pods/fulltext/src/manager.ts index a584e1ff4ad..bcb1e4e0daf 100644 --- a/pods/fulltext/src/manager.ts +++ b/pods/fulltext/src/manager.ts @@ -33,7 +33,7 @@ import { import { type QueueSourced, type FulltextDBConfiguration } from '@hcengineering/server-indexer' import { generateToken } from '@hcengineering/server-token' import { type Event } from '@hcengineering/communication-sdk-types' -import { getClient as getHulylakeClient } from '@hcengineering/hulylake-client' +import { getWorkspaceClient as getHulylakeClient } from '@hcengineering/hulylake-client' import { WorkspaceIndexer } from './workspace' diff --git a/pods/fulltext/src/workspace.ts b/pods/fulltext/src/workspace.ts index 269c1469070..2b67cbb6516 100644 --- a/pods/fulltext/src/workspace.ts +++ b/pods/fulltext/src/workspace.ts @@ -39,7 +39,7 @@ import { FullTextIndexPipeline } from '@hcengineering/server-indexer' import { getConfig } from '@hcengineering/server-pipeline' import { generateToken } from '@hcengineering/server-token' import { Api as CommunicationApi } from '@hcengineering/communication-server' -import { type HulylakeClient } from '@hcengineering/hulylake-client' +import { type HulylakeWorkspaceClient } from '@hcengineering/hulylake-client' import { fulltextModelFilter } from './utils' @@ -62,7 +62,7 @@ export class WorkspaceIndexer { externalStorage: StorageAdapter, ftadapter: FullTextAdapter, contentAdapter: ContentTextAdapter, - hulylake: HulylakeClient, + hulylake: HulylakeWorkspaceClient, endpointProvider: (token: string) => Promise, listener?: FulltextListener ): Promise { diff --git a/pods/preview/src/providers/octet.ts b/pods/preview/src/providers/octet.ts new file mode 100644 index 00000000000..c5e16b621fa --- /dev/null +++ b/pods/preview/src/providers/octet.ts @@ -0,0 +1,37 @@ +// +// 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 { type MeasureContext, type WorkspaceUuid } from '@hcengineering/core' + +import { type PreviewFile, type PreviewMetadata, type PreviewProvider } from '../types' + +export class OctetStreamProvider implements PreviewProvider { + supports (contentType: string): boolean { + return contentType === 'application/octet-stream' + } + + async image (ctx: MeasureContext, workspace: WorkspaceUuid, name: string, contentType: string): Promise { + throw new Error('Cannot generate image preview for application/octet-stream') + } + + async metadata ( + ctx: MeasureContext, + workspace: WorkspaceUuid, + name: string, + contentType: string + ): Promise { + return {} + } +} diff --git a/pods/preview/src/providers/video.ts b/pods/preview/src/providers/video.ts index d386ab2a33d..80b8e1dd8ca 100644 --- a/pods/preview/src/providers/video.ts +++ b/pods/preview/src/providers/video.ts @@ -13,8 +13,9 @@ // limitations under the License. // -import { type MeasureContext, type WorkspaceUuid } from '@hcengineering/core' +import { systemAccountUuid, type MeasureContext, type WorkspaceUuid } from '@hcengineering/core' import { StorageAdapter } from '@hcengineering/server-core' +import { generateToken } from '@hcengineering/server-token' import { getImageMetadata } from '../metadata' import { TemporaryDir } from '../tempdir' @@ -22,10 +23,13 @@ import { type PreviewFile, type PreviewMetadata, type PreviewProvider } from '.. import { extractThumbnail } from '../utils/ffmpeg' export class VideoProvider implements PreviewProvider { + private readonly token: string constructor ( private readonly storage: StorageAdapter, private readonly tempDir: TemporaryDir - ) {} + ) { + this.token = generateToken(systemAccountUuid) + } supports (mimeType: string): boolean { return mimeType.startsWith('video/') @@ -36,7 +40,7 @@ export class VideoProvider implements PreviewProvider { const url = await this.storage.getUrl(ctx, { uuid: workspace } as any, name) try { - await extractThumbnail(url, pngFile) + await extractThumbnail(url, pngFile, this.token) } catch (err: any) { // remove temporary png file in case of error this.tempDir.rm(pngFile) diff --git a/pods/preview/src/service.ts b/pods/preview/src/service.ts index b907bf5af8e..59b6ae4babc 100644 --- a/pods/preview/src/service.ts +++ b/pods/preview/src/service.ts @@ -23,6 +23,7 @@ import { TemporaryDir } from './tempdir' import { type PreviewFile, type PreviewMetadata, type PreviewProvider } from './types' import { transformImage } from './utils/sharp' import { SingleFlight } from './singleflight' +import { OctetStreamProvider } from './providers/octet' export interface ThumbnailParams { fit: 'cover' | 'contain' @@ -53,6 +54,7 @@ export function createPreviewService ( new DocProvider(storage, tempDir), new PdfProvider(storage, tempDir), new VideoProvider(storage, tempDir), + new OctetStreamProvider(), new FallbackProvider(imageProvider) ] return new PreviewServiceImpl(storage, cache, tempDir, providers, concurrency) @@ -112,7 +114,10 @@ class PreviewServiceImpl implements PreviewService { }) const thumbPath = this.tempDir.tmpFile() - const { contentType } = await transformImage(image.filePath, thumbPath, params) + const { contentType } = await ctx.with('transformImage', { format: params.format }, () => + transformImage(image.filePath, thumbPath, params) + ) + return { filePath: thumbPath, mimeType: contentType diff --git a/pods/preview/src/utils/ffmpeg.ts b/pods/preview/src/utils/ffmpeg.ts index 132e177ec82..cb284431822 100644 --- a/pods/preview/src/utils/ffmpeg.ts +++ b/pods/preview/src/utils/ffmpeg.ts @@ -15,9 +15,28 @@ import { spawn } from 'child_process' -export async function extractThumbnail (url: string, path: string, timestamp = '00:00:01'): Promise { +export async function extractThumbnail ( + url: string, + path: string, + token: string, + timestamp = '00:00:01' +): Promise { await new Promise((resolve, reject) => { - const ffmpeg = spawn('ffmpeg', ['-i', url, '-ss', timestamp, '-frames:v', '1', '-q:v', '2', '-y', path]) + const args = [ + '-headers', + `Authorization: Bearer ${token}`, + '-i', + url, + '-ss', + timestamp, + '-frames:v', + '1', + '-q:v', + '2', + '-y', + path + ] + const ffmpeg = spawn('ffmpeg', args) let error = '' diff --git a/rush.json b/rush.json index 45a14de4947..149a3b1569c 100644 --- a/rush.json +++ b/rush.json @@ -1531,6 +1531,11 @@ "projectFolder": "server/datalake", "shouldPublish": false }, + { + "packageName": "@hcengineering/hulylake", + "projectFolder": "server/hulylake", + "shouldPublish": false + }, { "packageName": "@hcengineering/bitrix", "projectFolder": "plugins/bitrix", diff --git a/server/hulylake/.eslintrc.js b/server/hulylake/.eslintrc.js new file mode 100644 index 00000000000..ce90fb9646f --- /dev/null +++ b/server/hulylake/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/server/hulylake/.npmignore b/server/hulylake/.npmignore new file mode 100644 index 00000000000..e3ec093c383 --- /dev/null +++ b/server/hulylake/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/server/hulylake/config/rig.json b/server/hulylake/config/rig.json new file mode 100644 index 00000000000..78cc5a17334 --- /dev/null +++ b/server/hulylake/config/rig.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig", + "rigProfile": "node" +} diff --git a/server/hulylake/jest.config.js b/server/hulylake/jest.config.js new file mode 100644 index 00000000000..2cfd408b679 --- /dev/null +++ b/server/hulylake/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/server/hulylake/package.json b/server/hulylake/package.json new file mode 100644 index 00000000000..aff1f189372 --- /dev/null +++ b/server/hulylake/package.json @@ -0,0 +1,44 @@ +{ + "name": "@hcengineering/hulylake", + "version": "0.6.0", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "author": "Anticrm Platform Contributors", + "template": "@hcengineering/node-package", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "build:watch": "compile", + "test": "jest --passWithNoTests --silent --forceExit", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent --forceExit", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "^0.6.0", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "@typescript-eslint/parser": "^6.11.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.1.0", + "typescript": "^5.8.3", + "@types/node": "^22.15.29", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "ts-node": "^10.8.0" + }, + "dependencies": { + "@hcengineering/core": "^0.6.32", + "@hcengineering/platform": "^0.6.11", + "@hcengineering/server-core": "^0.6.1", + "@hcengineering/server-token": "^0.6.11", + "@hcengineering/hulylake-client": "^0.6.0" + } +} diff --git a/server/hulylake/src/__tests__/utils.test.ts b/server/hulylake/src/__tests__/utils.test.ts new file mode 100644 index 00000000000..ab989104073 --- /dev/null +++ b/server/hulylake/src/__tests__/utils.test.ts @@ -0,0 +1,40 @@ +// +// 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 { wrapETag, unwrapETag } from '../utils' + +describe('unwrapETag', () => { + it('should unwrap weak validator prefix', () => { + expect(unwrapETag('W/"abc"')).toBe('abc') + }) + + it('should unwrap strong validator prefix', () => { + expect(unwrapETag('"abc"')).toBe('abc') + }) + + it('should unwrap no validator prefix', () => { + expect(unwrapETag('abc')).toBe('abc') + }) +}) + +describe('wrapETag', () => { + it('should wrap strong validator prefix', () => { + expect(wrapETag('abc')).toBe('"abc"') + }) + + it('should wrap weak validator prefix', () => { + expect(wrapETag('abc', true)).toBe('W/"abc"') + }) +}) diff --git a/server/hulylake/src/error.ts b/server/hulylake/src/error.ts new file mode 100644 index 00000000000..b5b60262b14 --- /dev/null +++ b/server/hulylake/src/error.ts @@ -0,0 +1,35 @@ +// +// Copyright © 2024 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. +// + +export class NetworkError extends Error { + constructor (message: string) { + super(message) + this.name = 'NetworkError' + } +} + +export class DatalakeError extends Error { + constructor (message: string) { + super(message) + this.name = 'DatalakeError' + } +} + +export class NotFoundError extends DatalakeError { + constructor (message = 'Not Found') { + super(message) + this.name = 'NotFoundError' + } +} diff --git a/server/hulylake/src/index.ts b/server/hulylake/src/index.ts new file mode 100644 index 00000000000..feec56529fb --- /dev/null +++ b/server/hulylake/src/index.ts @@ -0,0 +1,256 @@ +// +// Copyright © 2024 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 core, { + type Blob, + type MeasureContext, + type Ref, + type Timestamp, + type WorkspaceIds, + systemAccountUuid, + withContext +} from '@hcengineering/core' +import { type HulylakeClient, type PutOptions, getClient } from '@hcengineering/hulylake-client' +import { getMetadata } from '@hcengineering/platform' +import { + type BlobStorageIterator, + type BucketInfo, + type StorageAdapter, + type StorageConfig, + type StorageConfiguration, + type UploadedObjectInfo +} from '@hcengineering/server-core' +import serverToken, { generateToken } from '@hcengineering/server-token' +import { Readable } from 'stream' +import { NotFoundError } from './error' + +/** + * @public + */ +export interface HulylakeConfig extends StorageConfig { + kind: 'hulylake' +} + +/** + * @public + */ +export function createHulylakeClient (cfg: HulylakeConfig, token: string): HulylakeClient { + const endpoint = Number.isInteger(cfg.port) ? `${cfg.endpoint}:${cfg.port}` : cfg.endpoint + return getClient(endpoint, token) +} + +export const CONFIG_KIND = 'hulylake' + +/** + * @public + */ +export interface HulylakeClientOptions { + retryCount?: number + retryInterval?: number +} + +/** + * @public + */ +export class HulylakeService implements StorageAdapter { + private readonly client: HulylakeClient + private readonly retryCount: number + private readonly retryInterval: number + + constructor ( + readonly cfg: HulylakeConfig, + readonly options: HulylakeClientOptions = {} + ) { + const secret = getMetadata(serverToken.metadata.Secret) + if (secret === undefined) { + console.warn('Server secret not set, hulylake storage adapter initialized with default secret') + } + const token = generateToken(systemAccountUuid, undefined) + this.client = createHulylakeClient(cfg, token) + this.retryCount = options.retryCount ?? 5 + this.retryInterval = options.retryInterval ?? 50 + } + + async initialize (ctx: MeasureContext, wsIds: WorkspaceIds): Promise {} + + async close (): Promise {} + + async exists (ctx: MeasureContext, wsIds: WorkspaceIds): Promise { + // workspace/buckets not supported, assume that always exist + return true + } + + @withContext('make') + async make (ctx: MeasureContext, wsIds: WorkspaceIds): Promise { + // workspace/buckets not supported, assume that always exist + } + + async listBuckets (ctx: MeasureContext): Promise { + return [] + } + + @withContext('remove') + async remove (ctx: MeasureContext, wsIds: WorkspaceIds, objectNames: string[]): Promise { + await Promise.all( + objectNames.map(async (objectName) => { + await this.client.delete(wsIds.uuid, objectName) + }) + ) + } + + @withContext('delete') + async delete (ctx: MeasureContext, wsIds: WorkspaceIds): Promise { + // not supported, just do nothing and pretend we deleted the workspace + } + + @withContext('listStream') + async listStream (ctx: MeasureContext, wsIds: WorkspaceIds): Promise { + throw new Error('not implemented') + } + + @withContext('stat') + async stat (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string): Promise { + const result = await this.client.head(wsIds.uuid, objectName) + if (result.ok) { + return { + provider: '', + _class: core.class.Blob, + _id: objectName as Ref, + contentType: result.contentType ?? 'application/octet-stream', + size: result.contentLength ?? 0, + etag: result.etag ?? '', + space: core.space.Configuration, + modifiedBy: core.account.System, + modifiedOn: result.lastModified as Timestamp, + version: null + } + } + } + + @withContext('get') + async get (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string): Promise { + const res = await this.client.get(wsIds.uuid, objectName) + if (res?.body === undefined) { + throw new NotFoundError() + } + + return fromFetchBody(res.body) + } + + @withContext('put') + async put ( + ctx: MeasureContext, + wsIds: WorkspaceIds, + objectName: string, + stream: Readable | Buffer | string, + contentType: string, + size?: number + ): Promise { + size = size ?? (typeof stream === 'string' ? Buffer.byteLength(stream) : undefined) + const body = toFetchBody(stream) + + if (size === undefined) { + throw new Error('Size must be specified for string or stream body') + } + + const params: PutOptions = { + contentLength: size, + contentType + } + + const { etag } = await ctx.with('put', {}, (ctx) => this.client.put(wsIds.uuid, objectName, body, params), { + workspace: wsIds.uuid, + objectName + }) + + return { + etag: etag ?? '', + versionId: '' + } + } + + @withContext('read') + async read (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string): Promise { + const res = await this.client.get(wsIds.uuid, objectName) + if (res?.body === undefined) { + throw new NotFoundError() + } + + const body = fromFetchBody(res.body) + + const chunks: Buffer[] = [] + for await (const chunk of body) { + chunks.push(chunk) + } + + return chunks + } + + @withContext('partial') + async partial ( + ctx: MeasureContext, + wsIds: WorkspaceIds, + objectName: string, + offset: number, + length?: number + ): Promise { + const res = await this.client.partial(wsIds.uuid, objectName, offset, length) + if (res?.body === undefined) { + throw new NotFoundError() + } + + return fromFetchBody(res.body) + } + + async getUrl (ctx: MeasureContext, wsIds: WorkspaceIds, objectName: string): Promise { + return this.client.objectUrl(wsIds.uuid, objectName) + } +} + +export function processConfigFromEnv (storageConfig: StorageConfiguration): string | undefined { + const endpoint = process.env.HULYLAKE_ENDPOINT + if (endpoint === undefined) { + return 'HULYLAKE_ENDPOINT' + } + + const config: HulylakeConfig = { + kind: 'hulylake', + name: 'hulylake', + endpoint + } + + storageConfig.storages.push(config) + storageConfig.default = 'hulylake' +} + +function toFetchBody (body: Readable | Buffer | string): ReadableStream | ArrayBuffer | string { + if (typeof body === 'string') { + return body + } else if (Buffer.isBuffer(body)) { + return new Uint8Array(body).buffer + } else { + return Readable.toWeb(body) as ReadableStream + } +} + +function fromFetchBody (body: ReadableStream | ArrayBuffer | string): Readable { + if (typeof body === 'string') { + return Readable.from(body) + } else if (body instanceof ArrayBuffer) { + return Readable.from(Buffer.from(body)) + } else { + return Readable.fromWeb(body as any) + } +} diff --git a/server/hulylake/src/utils.ts b/server/hulylake/src/utils.ts new file mode 100644 index 00000000000..42d60ff1283 --- /dev/null +++ b/server/hulylake/src/utils.ts @@ -0,0 +1,32 @@ +// +// 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. +// + +export function unwrapETag (etag: string): string { + if (etag.startsWith('W/')) { + etag = etag.substring(2) + } + + if (etag.startsWith('"') && etag.endsWith('"')) { + etag = etag.slice(1, -1) + } + + return etag +} + +export function wrapETag (etag: string, weak: boolean = false): string { + etag = unwrapETag(etag) + const quoted = etag.startsWith('"') ? etag : `"${etag}"` + return weak ? `W/${quoted}` : quoted +} diff --git a/server/hulylake/tsconfig.json b/server/hulylake/tsconfig.json new file mode 100644 index 00000000000..c6a877cf6c3 --- /dev/null +++ b/server/hulylake/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/node/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/server/indexer/src/indexer/indexer.ts b/server/indexer/src/indexer/indexer.ts index e4aa7f57e7f..d460572b653 100644 --- a/server/indexer/src/indexer/indexer.ts +++ b/server/indexer/src/indexer/indexer.ts @@ -94,7 +94,7 @@ import { loadMessagesGroups } from '@hcengineering/communication-shared' import { markdownToMarkup } from '@hcengineering/text-markdown' -import { type HulylakeClient } from '@hcengineering/hulylake-client' +import { type HulylakeWorkspaceClient } from '@hcengineering/hulylake-client' export * from './types' export * from './utils' @@ -237,7 +237,7 @@ export class FullTextIndexPipeline implements FullTextPipeline { readonly storageAdapter: StorageAdapter, readonly contentAdapter: ContentTextAdapter, readonly broadcastUpdate: (ctx: MeasureContext, classes: Ref>[]) => void, - readonly hulylake: HulylakeClient, + readonly hulylake: HulylakeWorkspaceClient, readonly communicationApi?: CommunicationApi, readonly listener?: FulltextListener ) { diff --git a/server/server-storage/package.json b/server/server-storage/package.json index 4136ba81bb6..88510f17ba1 100644 --- a/server/server-storage/package.json +++ b/server/server-storage/package.json @@ -47,6 +47,7 @@ "@hcengineering/minio": "^0.6.0", "@hcengineering/s3": "^0.6.0", "@hcengineering/datalake": "^0.6.0", + "@hcengineering/hulylake": "^0.6.0", "@hcengineering/storage": "^0.6.0", "@hcengineering/analytics": "^0.6.0", "@hcengineering/server-token": "^0.6.11" diff --git a/server/server-storage/src/starter.ts b/server/server-storage/src/starter.ts index ac33f7b306e..6ae8afc1a69 100644 --- a/server/server-storage/src/starter.ts +++ b/server/server-storage/src/starter.ts @@ -1,4 +1,5 @@ import { CONFIG_KIND as DATALAKE_CONFIG_KIND, DatalakeService, type DatalakeConfig } from '@hcengineering/datalake' +import { CONFIG_KIND as HULYLAKE_CONFIG_KIND, HulylakeService, type HulylakeConfig } from '@hcengineering/hulylake' import { CONFIG_KIND as MINIO_CONFIG_KIND, MinioConfig, MinioService, addMinioFallback } from '@hcengineering/minio' import { CONFIG_KIND as S3_CONFIG_KIND, S3Service, type S3Config } from '@hcengineering/s3' import { StorageAdapter, StorageConfiguration, type StorageConfig } from '@hcengineering/server-core' @@ -97,6 +98,12 @@ export function createStorageFromConfig (config: StorageConfig): StorageAdapter throw new Error('Endpoint value is not specified') } adapter = new DatalakeService(c) + } else if (kind === HULYLAKE_CONFIG_KIND) { + const c = config as HulylakeConfig + if (c.endpoint == null) { + throw new Error('Endpoint value is not specified') + } + adapter = new HulylakeService(c) } else { throw new Error('Unsupported storage kind:' + kind) } diff --git a/services/translate/src/storage.ts b/services/translate/src/storage.ts index 06bb4276746..1a051735f04 100644 --- a/services/translate/src/storage.ts +++ b/services/translate/src/storage.ts @@ -12,7 +12,7 @@ // limitations under the License. import { MeasureContext, systemAccountUuid, WorkspaceUuid } from '@hcengineering/core' -import { getClient, type HulylakeClient, type JsonPatch } from '@hcengineering/hulylake-client' +import { type HulylakeWorkspaceClient, type JsonPatch, getWorkspaceClient } from '@hcengineering/hulylake-client' import { generateToken } from '@hcengineering/server-token' import { BlobID, @@ -39,9 +39,9 @@ export class Storage { constructor (private readonly ctx: MeasureContext) {} - private getClient (ws: WorkspaceUuid): HulylakeClient { + private getClient (ws: WorkspaceUuid): HulylakeWorkspaceClient { const token = generateToken(systemAccountUuid, ws, undefined, config.Secret) - return getClient(config.HulylakeUrl, ws, token) + return getWorkspaceClient(config.HulylakeUrl, ws, token) } private async createMessageGroup (ws: WorkspaceUuid, cardId: CardID, blobId: BlobID, lang: string): Promise {