import type { Client } from "openapi-fetch"; import createClient from "openapi-fetch"; import type { components, paths } from "./types"; // Generated by openapi-typescript import type { Database } from "../database/database"; import type { SyncSettings } from "../database/sync-settings"; import type { DocumentId, RelativePath, VaultUpdateId } from "src/database/document-metadata"; import { Logger } from "src/tracing/logger"; import { retriedFetch } from "src/utils/retried-fetch"; export interface CheckConnectionResult { isSuccessful: boolean; message: string; } export class SyncService { private client: Client; private clientWithoutRetries: Client; public constructor(private readonly database: Database) { this.createClient(database.getSettings()); database.addOnSettingsChangeHandlers((s) => { this.createClient(s); }); } private static formatError( error: components["schemas"]["SerializedError"] ): string { let result = error.message; if (error.causes.length > 0) { const causes = error.causes.join(", "); result += ` caused by: ${causes}`; } return result; } public async ping(): Promise { const response = await this.clientWithoutRetries.GET("/ping", { params: { header: { authorization: `Bearer ${this.database.getSettings().token}` } } }); Logger.getInstance().debug( `Ping response: ${JSON.stringify(response.data)}` ); if (!response.data) { throw new Error( `Failed to ping server: ${SyncService.formatError(response.error)}` ); } return response.data; } public async create({ relativePath, contentBytes, createdDate }: { relativePath: RelativePath; contentBytes: Uint8Array; createdDate: Date; }): Promise { const formData = new FormData(); formData.append("relative_path", relativePath); formData.append("created_date", createdDate.toISOString()); formData.append("content", new Blob([contentBytes])); const response = await this.client.POST( "/vaults/{vault_id}/documents", { params: { path: { vault_id: this.database.getSettings().vaultName }, header: { authorization: `Bearer ${this.database.getSettings().token}` } }, // eslint-disable-next-line body: formData as any // FormData is not supported by openapi-fetch } ); if (!response.data) { throw new Error( `Failed to create document: ${SyncService.formatError(response.error)}` ); } Logger.getInstance().debug( `Created document ${JSON.stringify(response.data)} with id ${ response.data.documentId }` ); return response.data; } public async put({ parentVersionId, documentId, relativePath, contentBytes, createdDate }: { parentVersionId: VaultUpdateId; documentId: DocumentId; relativePath: RelativePath; contentBytes: Uint8Array; createdDate: Date; }): Promise { const formData = new FormData(); formData.append("parent_version_id", parentVersionId.toString()); formData.append("created_date", createdDate.toISOString()); formData.append("relative_path", relativePath); formData.append("content", new Blob([contentBytes])); const response = await this.client.PUT( "/vaults/{vault_id}/documents/{document_id}", { params: { path: { vault_id: this.database.getSettings().vaultName, document_id: documentId }, header: { authorization: `Bearer ${this.database.getSettings().token}` } }, // eslint-disable-next-line body: formData as any // FormData is not supported by openapi-fetch } ); if (!response.data) { throw new Error( `Failed to update document: ${SyncService.formatError(response.error)}` ); } Logger.getInstance().debug( `Updated document ${JSON.stringify(response.data)} with id ${ response.data.documentId }` ); return response.data; } public async delete({ documentId, relativePath, createdDate }: { documentId: DocumentId; relativePath: RelativePath; createdDate: Date; }): Promise { const response = await this.client.DELETE( "/vaults/{vault_id}/documents/{document_id}", { params: { path: { vault_id: this.database.getSettings().vaultName, document_id: documentId }, header: { authorization: `Bearer ${this.database.getSettings().token}` } }, body: { createdDate: createdDate.toISOString(), relativePath } } ); if (response.error) { throw new Error(`Failed to delete document`); } Logger.getInstance().debug( `Deleted document ${relativePath} with id ${documentId}` ); return response.data; } public async get({ documentId }: { documentId: DocumentId; }): Promise { const response = await this.client.GET( "/vaults/{vault_id}/documents/{document_id}", { params: { path: { vault_id: this.database.getSettings().vaultName, document_id: documentId }, header: { authorization: `Bearer ${this.database.getSettings().token}` } } } ); if (!response.data) { throw new Error( `Failed to get document: ${SyncService.formatError(response.error)}` ); } Logger.getInstance().debug( `Get document ${response.data.relativePath} with id ${response.data.documentId}` ); return response.data; } public async getAll( since?: VaultUpdateId ): Promise { const response = await this.client.GET("/vaults/{vault_id}/documents", { params: { path: { vault_id: this.database.getSettings().vaultName }, header: { authorization: `Bearer ${this.database.getSettings().token}` }, query: { since_update_id: since } } }); const { error } = response; if (error) { throw new Error( `Failed to get documents: ${SyncService.formatError(response.error)}` ); } Logger.getInstance().debug( `Got ${response.data.latestDocuments.length} document metadata` ); return response.data; } public async checkConnection(): Promise { try { const result = await this.ping(); if (result.isAuthenticated) { return { isSuccessful: true, message: `Successfully connected to server (version: ${result.serverVersion}) and authenticated.` }; } return { isSuccessful: false, message: `Successfully connected to server (version: ${result.serverVersion}) but failed to authenticate.` }; } catch (e) { return { isSuccessful: false, message: `Failed to connect to server: ${e}` }; } } private createClient(settings: SyncSettings): void { this.client = createClient({ baseUrl: settings.remoteUri, fetch: retriedFetch }); this.clientWithoutRetries = createClient({ baseUrl: settings.remoteUri }); } }