diff --git a/src/backup/backupCreateStatusGetter.ts b/src/backup/backupCreateStatusGetter.ts index 3a0c680c..ea131685 100644 --- a/src/backup/backupCreateStatusGetter.ts +++ b/src/backup/backupCreateStatusGetter.ts @@ -1,4 +1,5 @@ import Connection from '../connection/index.js'; +import { WeaviateInvalidInputError } from '../errors.js'; import { BackupCreateStatusResponse } from '../openapi/types.js'; import { CommandBase } from '../validation/commandBase.js'; import { Backend } from './index.js'; @@ -29,7 +30,7 @@ export default class BackupCreateStatusGetter extends CommandBase { do = (): Promise => { this.validate(); if (this.errors.length > 0) { - return Promise.reject(new Error('invalid usage: ' + this.errors.join(', '))); + return Promise.reject(new WeaviateInvalidInputError('invalid usage: ' + this.errors.join(', '))); } return this.client.get(this._path()) as Promise; }; diff --git a/src/backup/backupCreator.ts b/src/backup/backupCreator.ts index 1c1b7908..5e9d5697 100644 --- a/src/backup/backupCreator.ts +++ b/src/backup/backupCreator.ts @@ -1,4 +1,5 @@ import Connection from '../connection/index.js'; +import { WeaviateInvalidInputError } from '../errors.js'; import { BackupConfig, BackupCreateRequest, @@ -81,7 +82,7 @@ export default class BackupCreator extends CommandBase { do = (): Promise => { this.validate(); if (this.errors.length > 0) { - return Promise.reject(new Error('invalid usage: ' + this.errors.join(', '))); + return Promise.reject(new WeaviateInvalidInputError('invalid usage: ' + this.errors.join(', '))); } const payload = { diff --git a/src/backup/backupGetter.ts b/src/backup/backupGetter.ts index a9d0905d..71c1142f 100644 --- a/src/backup/backupGetter.ts +++ b/src/backup/backupGetter.ts @@ -1,4 +1,5 @@ import Connection from '../connection/index.js'; +import { WeaviateInvalidInputError } from '../errors.js'; import { BackupCreateResponse } from '../openapi/types.js'; import { CommandBase } from '../validation/commandBase.js'; import { Backend } from './index.js'; @@ -23,7 +24,7 @@ export default class BackupGetter extends CommandBase { do = (): Promise => { this.validate(); if (this.errors.length > 0) { - return Promise.reject(new Error('invalid usage: ' + this.errors.join(', '))); + return Promise.reject(new WeaviateInvalidInputError('invalid usage: ' + this.errors.join(', '))); } return this.client.get(this._path()); diff --git a/src/backup/backupRestoreStatusGetter.ts b/src/backup/backupRestoreStatusGetter.ts index fa59afda..3599287f 100644 --- a/src/backup/backupRestoreStatusGetter.ts +++ b/src/backup/backupRestoreStatusGetter.ts @@ -1,4 +1,5 @@ import Connection from '../connection/index.js'; +import { WeaviateInvalidInputError } from '../errors.js'; import { BackupRestoreStatusResponse } from '../openapi/types.js'; import { CommandBase } from '../validation/commandBase.js'; import { Backend } from './index.js'; @@ -29,7 +30,7 @@ export default class BackupRestoreStatusGetter extends CommandBase { do = (): Promise => { this.validate(); if (this.errors.length > 0) { - return Promise.reject(new Error('invalid usage: ' + this.errors.join(', '))); + return Promise.reject(new WeaviateInvalidInputError('invalid usage: ' + this.errors.join(', '))); } return this.client.get(this._path()); diff --git a/src/backup/backupRestorer.ts b/src/backup/backupRestorer.ts index a4acb226..e2f76c0f 100644 --- a/src/backup/backupRestorer.ts +++ b/src/backup/backupRestorer.ts @@ -1,4 +1,5 @@ import Connection from '../connection/index.js'; +import { WeaviateInvalidInputError } from '../errors.js'; import { BackupRestoreRequest, BackupRestoreResponse, @@ -81,7 +82,7 @@ export default class BackupRestorer extends CommandBase { do = (): Promise => { this.validate(); if (this.errors.length > 0) { - return Promise.reject(new Error('invalid usage: ' + this.errors.join(', '))); + return Promise.reject(new WeaviateInvalidInputError('invalid usage: ' + this.errors.join(', '))); } const payload = { diff --git a/src/collections/backup/client.ts b/src/collections/backup/client.ts index 83262618..b24f12bd 100644 --- a/src/collections/backup/client.ts +++ b/src/collections/backup/client.ts @@ -1,75 +1,111 @@ import { Backend, - BackupCompressionLevel, BackupCreateStatusGetter, BackupCreator, BackupRestoreStatusGetter, BackupRestorer, } from '../../backup/index.js'; +import { validateBackend, validateBackupId } from '../../backup/validation.js'; import Connection from '../../connection/index.js'; -import { WeaviateBackupFailed } from '../../errors.js'; +import { + WeaviateBackupCanceled, + WeaviateBackupCancellationError, + WeaviateBackupFailed, + WeaviateInvalidInputError, + WeaviateUnexpectedResponseError, + WeaviateUnexpectedStatusCodeError, +} from '../../errors.js'; import { BackupCreateResponse, BackupCreateStatusResponse, BackupRestoreResponse, - BackupRestoreStatusResponse, } from '../../openapi/types.js'; - -/** Configuration options available when creating a backup */ -export type BackupConfigCreate = { - /** The size of the chunks to use for the backup. */ - chunkSize?: number; - /** The standard of compression to use for the backup. */ - compressionLevel?: BackupCompressionLevel; - /** The percentage of CPU to use for the backup creation job. */ - cpuPercentage?: number; -}; - -/** Configuration options available when restoring a backup */ -export type BackupConfigRestore = { - /** The percentage of CPU to use for the backuop restoration job. */ - cpuPercentage?: number; -}; - -/** The arguments required to create and restore backups. */ -export type BackupArgs = { - /** The ID of the backup. */ - backupId: string; - /** The backend to use for the backup. */ - backend: Backend; - /** The collections to include in the backup. */ - includeCollections?: string[]; - /** The collections to exclude from the backup. */ - excludeCollections?: string[]; - /** Whether to wait for the backup to complete. */ - waitForCompletion?: boolean; - /** The configuration options for the backup. */ - config?: C; -}; - -/** The arguments required to get the status of a backup. */ -export type BackupStatusArgs = { - /** The ID of the backup. */ - backupId: string; - /** The backend to use for the backup. */ - backend: Backend; -}; +import { + BackupArgs, + BackupCancelArgs, + BackupConfigCreate, + BackupConfigRestore, + BackupReturn, + BackupStatusArgs, + BackupStatusReturn, +} from './types.js'; export const backup = (connection: Connection) => { - const getCreateStatus = (args: BackupStatusArgs): Promise => { + const parseStatus = (res: BackupCreateStatusResponse | BackupRestoreResponse): BackupStatusReturn => { + if (res.id === undefined) { + throw new WeaviateUnexpectedResponseError('Backup ID is undefined in response'); + } + if (res.path === undefined) { + throw new WeaviateUnexpectedResponseError('Backup path is undefined in response'); + } + if (res.status === undefined) { + throw new WeaviateUnexpectedResponseError('Backup status is undefined in response'); + } + return { + id: res.id, + error: res.error, + path: res.path, + status: res.status, + }; + }; + const parseResponse = (res: BackupCreateResponse | BackupRestoreResponse): BackupReturn => { + if (res.id === undefined) { + throw new WeaviateUnexpectedResponseError('Backup ID is undefined in response'); + } + if (res.backend === undefined) { + throw new WeaviateUnexpectedResponseError('Backup backend is undefined in response'); + } + if (res.path === undefined) { + throw new WeaviateUnexpectedResponseError('Backup path is undefined in response'); + } + if (res.status === undefined) { + throw new WeaviateUnexpectedResponseError('Backup status is undefined in response'); + } + return { + id: res.id, + backend: res.backend as Backend, + collections: res.classes ? res.classes : [], + error: res.error, + path: res.path, + status: res.status, + }; + }; + const getCreateStatus = (args: BackupStatusArgs): Promise => { return new BackupCreateStatusGetter(connection) .withBackupId(args.backupId) .withBackend(args.backend) - .do(); + .do() + .then(parseStatus); }; - const getRestoreStatus = (args: BackupStatusArgs): Promise => { + const getRestoreStatus = (args: BackupStatusArgs): Promise => { return new BackupRestoreStatusGetter(connection) .withBackupId(args.backupId) .withBackend(args.backend) - .do(); + .do() + .then(parseStatus); }; return { - create: async (args: BackupArgs): Promise => { + cancel: async (args: BackupCancelArgs): Promise => { + let errors: string[] = []; + errors = errors.concat(validateBackupId(args.backupId)).concat(validateBackend(args.backend)); + if (errors.length > 0) { + throw new WeaviateInvalidInputError(errors.join(', ')); + } + + try { + await connection.delete(`/backups/${args.backend}/${args.backupId}`, undefined, false); + } catch (err) { + if (err instanceof WeaviateUnexpectedStatusCodeError) { + if (err.code === 404) { + return false; + } + throw new WeaviateBackupCancellationError(err.message); + } + } + + return true; + }, + create: async (args: BackupArgs): Promise => { let builder = new BackupCreator(connection, new BackupCreateStatusGetter(connection)) .withBackupId(args.backupId) .withBackend(args.backend); @@ -90,31 +126,34 @@ export const backup = (connection: Connection) => { try { res = await builder.do(); } catch (err) { - throw new Error(`Backup creation failed: ${err}`); + throw new WeaviateBackupFailed(`Backup creation failed: ${err}`, 'creation'); } if (res.status === 'FAILED') { - throw new Error(`Backup creation failed: ${res.error}`); + throw new WeaviateBackupFailed(`Backup creation failed: ${res.error}`, 'creation'); } - let status: BackupCreateStatusResponse | undefined; + let status: BackupStatusReturn | undefined; if (args.waitForCompletion) { let wait = true; while (wait) { - const res = await getCreateStatus(args); // eslint-disable-line no-await-in-loop - if (res.status === 'SUCCESS') { + const ret = await getCreateStatus(args); // eslint-disable-line no-await-in-loop + if (ret.status === 'SUCCESS') { wait = false; - status = res; + status = ret; + } + if (ret.status === 'FAILED') { + throw new WeaviateBackupFailed(ret.error ? ret.error : '', 'creation'); } - if (res.status === 'FAILED') { - throw new WeaviateBackupFailed(res.error ? res.error : '', 'creation'); + if (ret.status === 'CANCELED') { + throw new WeaviateBackupCanceled('creation'); } await new Promise((resolve) => setTimeout(resolve, 1000)); // eslint-disable-line no-await-in-loop } } - return status ? { ...status, classes: res.classes } : res; + return status ? { ...parseResponse(res), ...status } : parseResponse(res); }, getCreateStatus: getCreateStatus, getRestoreStatus: getRestoreStatus, - restore: async (args: BackupArgs): Promise => { + restore: async (args: BackupArgs): Promise => { let builder = new BackupRestorer(connection, new BackupRestoreStatusGetter(connection)) .withBackupId(args.backupId) .withBackend(args.backend); @@ -133,63 +172,83 @@ export const backup = (connection: Connection) => { try { res = await builder.do(); } catch (err) { - throw new Error(`Backup restoration failed: ${err}`); + throw new WeaviateBackupFailed(`Backup restoration failed: ${err}`, 'restoration'); } if (res.status === 'FAILED') { - throw new Error(`Backup restoration failed: ${res.error}`); + throw new WeaviateBackupFailed(`Backup restoration failed: ${res.error}`, 'restoration'); } - let status: BackupRestoreStatusResponse | undefined; + let status: BackupStatusReturn | undefined; if (args.waitForCompletion) { let wait = true; while (wait) { - const res = await getRestoreStatus(args); // eslint-disable-line no-await-in-loop - if (res.status === 'SUCCESS') { + const ret = await getRestoreStatus(args); // eslint-disable-line no-await-in-loop + if (ret.status === 'SUCCESS') { wait = false; - status = res; + status = ret; + } + if (ret.status === 'FAILED') { + throw new WeaviateBackupFailed(ret.error ? ret.error : '', 'restoration'); } - if (res.status === 'FAILED') { - throw new WeaviateBackupFailed(res.error ? res.error : '', 'restoration'); + if (ret.status === 'CANCELED') { + throw new WeaviateBackupCanceled('restoration'); } await new Promise((resolve) => setTimeout(resolve, 1000)); // eslint-disable-line no-await-in-loop } } return status ? { + ...parseResponse(res), ...status, - classes: res.classes, } - : res; + : parseResponse(res); }, }; }; export interface Backup { + /** + * Cancel a backup. + * + * @param {BackupCancelArgs} args The arguments for the request. + * @returns {Promise} Whether the backup was canceled. + * @throws {WeaviateInvalidInputError} If the input is invalid. + * @throws {WeaviateBackupCancellationError} If the backup cancellation fails. + */ + cancel(args: BackupCancelArgs): Promise; /** * Create a backup of the database. * * @param {BackupArgs} args The arguments for the request. - * @returns {Promise} The response from Weaviate. + * @returns {Promise} The response from Weaviate. + * @throws {WeaviateInvalidInputError} If the input is invalid. + * @throws {WeaviateBackupFailed} If the backup creation fails. + * @throws {WeaviateBackupCanceled} If the backup creation is canceled. */ - create(args: BackupArgs): Promise; + create(args: BackupArgs): Promise; /** * Get the status of a backup creation. * * @param {BackupStatusArgs} args The arguments for the request. - * @returns {Promise} The status of the backup creation. + * @returns {Promise} The status of the backup creation. + * @throws {WeaviateInvalidInputError} If the input is invalid. */ - getCreateStatus(args: BackupStatusArgs): Promise; + getCreateStatus(args: BackupStatusArgs): Promise; /** * Get the status of a backup restore. * * @param {BackupStatusArgs} args The arguments for the request. - * @returns {Promise} The status of the backup restore. + * @returns {Promise} The status of the backup restore. + * @throws {WeaviateInvalidInputError} If the input is invalid. */ - getRestoreStatus(args: BackupStatusArgs): Promise; + getRestoreStatus(args: BackupStatusArgs): Promise; /** * Restore a backup of the database. * * @param {BackupArgs} args The arguments for the request. - * @returns {Promise} The response from Weaviate. + * @returns {Promise} The response from Weaviate. + * @throws {WeaviateInvalidInputError} If the input is invalid. + * @throws {WeaviateBackupFailed} If the backup restoration fails. + * @throws {WeaviateBackupCanceled} If the backup restoration is canceled. */ - restore(args: BackupArgs): Promise; + restore(args: BackupArgs): Promise; } diff --git a/src/collections/backup/collection.ts b/src/collections/backup/collection.ts index 956fbb33..89ffd864 100644 --- a/src/collections/backup/collection.ts +++ b/src/collections/backup/collection.ts @@ -1,12 +1,8 @@ import { Backend } from '../../backup/index.js'; import Connection from '../../connection/index.js'; -import { - BackupCreateResponse, - BackupCreateStatusResponse, - BackupRestoreResponse, - BackupRestoreStatusResponse, -} from '../../openapi/types.js'; -import { BackupStatusArgs, backup } from './client.js'; +import { WeaviateInvalidInputError } from '../../errors.js'; +import { backup } from './client.js'; +import { BackupReturn, BackupStatusArgs, BackupStatusReturn } from './types.js'; /** The arguments required to create and restore backups. */ export type BackupCollectionArgs = { @@ -41,28 +37,36 @@ export interface BackupCollection { * Create a backup of this collection. * * @param {BackupArgs} args The arguments for the request. - * @returns {Promise} The response from Weaviate. + * @returns {Promise} The response from Weaviate. + * @throws {WeaviateInvalidInputError} If the input is invalid. + * @throws {WeaviateBackupFailed} If the backup creation fails. + * @throws {WeaviateBackupCanceled} If the backup creation is canceled. */ - create(args: BackupCollectionArgs): Promise; + create(args: BackupCollectionArgs): Promise; /** * Get the status of a backup. * * @param {BackupStatusArgs} args The arguments for the request. - * @returns {Promise} The status of the backup. + * @returns {Promise} The status of the backup. + * @throws {WeaviateInvalidInputError} If the input is invalid. */ - getCreateStatus(args: BackupStatusArgs): Promise; + getCreateStatus(args: BackupStatusArgs): Promise; /** * Get the status of a restore. * * @param {BackupStatusArgs} args The arguments for the request. - * @returns {Promise} The status of the restore. + * @returns {Promise} The status of the restore. + * @throws {WeaviateInvalidInputError} If the input is invalid. */ - getRestoreStatus(args: BackupStatusArgs): Promise; + getRestoreStatus(args: BackupStatusArgs): Promise; /** * Restore a backup of this collection. * * @param {BackupArgs} args The arguments for the request. - * @returns {Promise} The response from Weaviate. + * @returns {Promise} The response from Weaviate. + * @throws {WeaviateInvalidInputError} If the input is invalid. + * @throws {WeaviateBackupFailed} If the backup restoration fails. + * @throws {WeaviateBackupCanceled} If the backup restoration is canceled. */ - restore(args: BackupCollectionArgs): Promise; + restore(args: BackupCollectionArgs): Promise; } diff --git a/src/collections/backup/index.ts b/src/collections/backup/index.ts index 5b5c3e11..e4c6f1b1 100644 --- a/src/collections/backup/index.ts +++ b/src/collections/backup/index.ts @@ -1,8 +1,3 @@ -export type { - Backup, - BackupArgs, - BackupConfigCreate, - BackupConfigRestore, - BackupStatusArgs, -} from './client.js'; +export type { Backup } from './client.js'; export type { BackupCollection, BackupCollectionArgs } from './collection.js'; +export type { BackupArgs, BackupConfigCreate, BackupConfigRestore, BackupStatusArgs } from './types.js'; diff --git a/src/collections/backup/integration.test.ts b/src/collections/backup/integration.test.ts index cf8888a6..1eb65358 100644 --- a/src/collections/backup/integration.test.ts +++ b/src/collections/backup/integration.test.ts @@ -72,7 +72,7 @@ describe('Integration testing of backups', () => { waitForCompletion: true, }); expect(res.status).toBe('SUCCESS'); - expect(res.classes).toEqual(['TestBackupCollection']); + expect(res.collections).toEqual(['TestBackupCollection']); return collection; }; @@ -82,7 +82,7 @@ describe('Integration testing of backups', () => { backend: 'filesystem', }); expect(res.status).toBe('STARTED'); - expect(res.classes).toEqual(['TestBackupCollection']); + expect(res.collections).toEqual(['TestBackupCollection']); const status = await collection.backup.getCreateStatus({ backupId: res.id as string, backend: res.backend as 'filesystem', diff --git a/src/collections/backup/types.ts b/src/collections/backup/types.ts new file mode 100644 index 00000000..250d88e6 --- /dev/null +++ b/src/collections/backup/types.ts @@ -0,0 +1,72 @@ +import { Backend, BackupCompressionLevel } from '../../index.js'; + +/** The status of a backup operation */ +export type BackupStatus = 'STARTED' | 'TRANSFERRING' | 'TRANSFERRED' | 'SUCCESS' | 'FAILED' | 'CANCELED'; + +/** The status of a backup operation */ +export type BackupStatusReturn = { + /** The ID of the backup */ + id: string; + /** The error message if the backup failed */ + error?: string; + /** The path to the backup */ + path: string; + /** The status of the backup */ + status: BackupStatus; +}; + +/** The return type of a backup creation or restoration operation */ +export type BackupReturn = BackupStatusReturn & { + /** The backend to which the backup was created or restored */ + backend: Backend; + /** The collections that were included in the backup */ + collections: string[]; +}; + +/** Configuration options available when creating a backup */ +export type BackupConfigCreate = { + /** The size of the chunks to use for the backup. */ + chunkSize?: number; + /** The standard of compression to use for the backup. */ + compressionLevel?: BackupCompressionLevel; + /** The percentage of CPU to use for the backup creation job. */ + cpuPercentage?: number; +}; + +/** Configuration options available when restoring a backup */ +export type BackupConfigRestore = { + /** The percentage of CPU to use for the backuop restoration job. */ + cpuPercentage?: number; +}; + +/** The arguments required to create and restore backups. */ +export type BackupArgs = { + /** The ID of the backup. */ + backupId: string; + /** The backend to use for the backup. */ + backend: Backend; + /** The collections to include in the backup. */ + includeCollections?: string[]; + /** The collections to exclude from the backup. */ + excludeCollections?: string[]; + /** Whether to wait for the backup to complete. */ + waitForCompletion?: boolean; + /** The configuration options for the backup. */ + config?: C; +}; + +/** The arguments required to get the status of a backup. */ +export type BackupStatusArgs = { + /** The ID of the backup. */ + backupId: string; + /** The backend to use for the backup. */ + backend: Backend; +}; + +/** The arguments required to cancel a backup. */ +export type BackupCancelArgs = { + /** The ID of the backup. */ + backupId: string; + /** The backend to use for the backup. */ + backend: Backend; +}; diff --git a/src/collections/backup/unit.test.ts b/src/collections/backup/unit.test.ts new file mode 100644 index 00000000..4bfcfa69 --- /dev/null +++ b/src/collections/backup/unit.test.ts @@ -0,0 +1,141 @@ +import express, { Response } from 'express'; +import { Server as HttpServer } from 'http'; +import { Server as GrpcServer, createServer } from 'nice-grpc'; +import { WeaviateBackupCanceled } from '../../errors'; +import weaviate, { WeaviateClient } from '../../index.js'; +import { + HealthCheckRequest, + HealthCheckResponse, + HealthCheckResponse_ServingStatus, + HealthDefinition, + HealthServiceImplementation, +} from '../../proto/google/health/v1/health'; +import { BackupCreateResponse, BackupCreateStatusResponse, BackupRestoreResponse } from '../../v2'; +import { BackupStatus } from './types'; + +const BACKUP_ID = 'test-backup-123'; +const BACKEND = 'filesystem'; + +class CancelMock { + private grpc: GrpcServer; + private http: HttpServer; + static status: BackupStatus; + + constructor(grpc: GrpcServer, http: HttpServer) { + this.grpc = grpc; + this.http = http; + } + + public static use = async (version: string, httpPort: number, grpcPort: number) => { + const httpApp = express(); + // Meta endpoint required for client instantiation + httpApp.get('/v1/meta', (req, res) => res.send({ version })); + + // Backup cancellation endpoint + httpApp.delete(`/v1/backups/${BACKEND}/${BACKUP_ID}`, (req, res) => { + CancelMock.status = 'CANCELED'; + res.send(); + }); + + // Backup creation endpoint + httpApp.post(`/v1/backups/${BACKEND}`, (req, res: Response) => { + CancelMock.status = 'STARTED'; + res.send({ + id: BACKUP_ID, + backend: BACKEND, + path: 'path/to/backup', + status: CancelMock.status, + }); + }); + // Backup creation status endpoint + httpApp.get( + `/v1/backups/${BACKEND}/${BACKUP_ID}`, + (req, res: Response) => + res.send({ + id: BACKUP_ID, + backend: BACKEND, + path: 'path/to/backup', + status: CancelMock.status, + }) + ); + + // Backup restoration endpoint + httpApp.post( + `/v1/backups/${BACKEND}/${BACKUP_ID}/restore`, + (req, res: Response) => { + CancelMock.status = 'STARTED'; + res.send({ + id: BACKUP_ID, + backend: BACKEND, + path: 'path/to/backup', + status: CancelMock.status, + }); + } + ); + // Backup restoration status endpoint + httpApp.get( + `/v1/backups/${BACKEND}/${BACKUP_ID}/restore`, + (req, res: Response) => + res.send({ + id: BACKUP_ID, + backend: BACKEND, + path: 'path/to/backup', + status: CancelMock.status, + }) + ); + + // gRPC health check required for client instantiation + const healthMockImpl: HealthServiceImplementation = { + check: (request: HealthCheckRequest): Promise => + Promise.resolve(HealthCheckResponse.create({ status: HealthCheckResponse_ServingStatus.SERVING })), + watch: jest.fn(), + }; + + const grpc = createServer(); + grpc.add(HealthDefinition, healthMockImpl); + + httpApp.on('error', (error) => console.error('HTTP Server Error:', error)); + + await grpc.listen(`localhost:${grpcPort}`); + const http = await httpApp.listen(httpPort); + return new CancelMock(grpc, http); + }; + + public close = () => Promise.all([this.http.close(), this.grpc.shutdown()]); +} + +describe('Mock testing of backup cancellation', () => { + let client: WeaviateClient; + let mock: CancelMock; + + beforeAll(async () => { + mock = await CancelMock.use('1.27.0', 8958, 8959); + client = await weaviate.connectToLocal({ port: 8958, grpcPort: 8959 }); + }); + + it('should throw while waiting for creation if backup is cancelled in the meantime', async () => { + const promise = client.backup.create({ + backupId: BACKUP_ID, + backend: BACKEND, + waitForCompletion: true, + }); + await client.backup.cancel({ backupId: BACKUP_ID, backend: BACKEND }); + try { + await promise; + } catch (err) { + expect(err).toBeInstanceOf(WeaviateBackupCanceled); + } + }); + + it('should return true if creation cancellation was successful', async () => { + const success = await client.backup.cancel({ backupId: BACKUP_ID, backend: BACKEND }); + expect(success).toBe(true); + }); + + it('should return false if creation backup does not exist', async () => { + const success = await client.backup.cancel({ backupId: `${BACKUP_ID}4`, backend: BACKEND }); + expect(success).toBe(false); + }); + + afterAll(() => mock.close()); +}); diff --git a/src/errors.ts b/src/errors.ts index ea754e5f..ccff65a7 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -96,6 +96,12 @@ export class WeaviateUnexpectedStatusCodeError extends WeaviateError { } } +export class WeaviateUnexpectedResponseError extends WeaviateError { + constructor(message: string) { + super(`The response from Weaviate was unexpected: ${message}`); + } +} + /** * Is thrown when a backup creation or restoration fails. */ @@ -105,6 +111,21 @@ export class WeaviateBackupFailed extends WeaviateError { } } +/** + * Is thrown when a backup creation or restoration fails. + */ +export class WeaviateBackupCanceled extends WeaviateError { + constructor(kind: 'creation' | 'restoration') { + super(`Backup ${kind} was canceled`); + } +} + +export class WeaviateBackupCancellationError extends WeaviateError { + constructor(message: string) { + super(`Backup cancellation failed with message: ${message}`); + } +} + /** * Is thrown if the Weaviate server does not support a feature that the client is trying to use. */ diff --git a/src/openapi/schema.ts b/src/openapi/schema.ts index d8ddf03b..3ffff0c4 100644 --- a/src/openapi/schema.ts +++ b/src/openapi/schema.ts @@ -147,12 +147,16 @@ export interface paths { head: operations['tenant.exists']; }; '/backups/{backend}': { + /** [Coming soon] List all backups in progress not implemented yet. */ + get: operations['backups.list']; /** Starts a process of creating a backup for a set of classes */ post: operations['backups.create']; }; '/backups/{backend}/{id}': { /** Returns status of backup creation attempt for a set of classes */ get: operations['backups.create.status']; + /** Cancel created backup with specified ID */ + delete: operations['backups.cancel']; }; '/backups/{backend}/{id}/restore': { /** Returns status of a backup restoration attempt for a set of classes */ @@ -344,6 +348,11 @@ export interface definitions { factor?: number; /** @description Enable asynchronous replication */ asyncEnabled?: boolean; + /** + * @description Conflict resolution strategy for deleted objects + * @enum {string} + */ + deletionStrategy?: 'NoAutomatedResolution' | 'DeleteOnConflict'; }; /** @description tuning parameters for the BM25 algorithm */ BM25Config: { @@ -572,7 +581,7 @@ export interface definitions { * @default STARTED * @enum {string} */ - status?: 'STARTED' | 'TRANSFERRING' | 'TRANSFERRED' | 'SUCCESS' | 'FAILED'; + status?: 'STARTED' | 'TRANSFERRING' | 'TRANSFERRED' | 'SUCCESS' | 'FAILED' | 'CANCELED'; }; /** @description The definition of a backup restore metadata */ BackupRestoreStatusResponse: { @@ -589,7 +598,7 @@ export interface definitions { * @default STARTED * @enum {string} */ - status?: 'STARTED' | 'TRANSFERRING' | 'TRANSFERRED' | 'SUCCESS' | 'FAILED'; + status?: 'STARTED' | 'TRANSFERRING' | 'TRANSFERRED' | 'SUCCESS' | 'FAILED' | 'CANCELED'; }; /** @description Backup custom configuration */ BackupConfig: { @@ -646,8 +655,22 @@ export interface definitions { * @default STARTED * @enum {string} */ - status?: 'STARTED' | 'TRANSFERRING' | 'TRANSFERRED' | 'SUCCESS' | 'FAILED'; + status?: 'STARTED' | 'TRANSFERRING' | 'TRANSFERRED' | 'SUCCESS' | 'FAILED' | 'CANCELED'; }; + /** @description The definition of a backup create response body */ + BackupListResponse: { + /** @description The ID of the backup. Must be URL-safe and work as a filesystem path, only lowercase, numbers, underscore, minus characters allowed. */ + id?: string; + /** @description destination path of backup files proper to selected backend */ + path?: string; + /** @description The list of classes for which the existed backup process */ + classes?: string[]; + /** + * @description status of backup process + * @enum {string} + */ + status?: 'STARTED' | 'TRANSFERRING' | 'TRANSFERRED' | 'SUCCESS' | 'FAILED' | 'CANCELED'; + }[]; /** @description Request body for restoring a backup for a set of classes */ BackupRestoreRequest: { /** @description Custom configuration for the backup restoration process */ @@ -676,7 +699,7 @@ export interface definitions { * @default STARTED * @enum {string} */ - status?: 'STARTED' | 'TRANSFERRING' | 'TRANSFERRED' | 'SUCCESS' | 'FAILED'; + status?: 'STARTED' | 'TRANSFERRING' | 'TRANSFERRED' | 'SUCCESS' | 'FAILED' | 'CANCELED'; }; /** @description The summary of Weaviate's statistics. */ NodeStats: { @@ -2685,6 +2708,35 @@ export interface operations { }; }; }; + /** [Coming soon] List all backups in progress not implemented yet. */ + 'backups.list': { + parameters: { + path: { + /** Backup backend name e.g. filesystem, gcs, s3. */ + backend: string; + }; + }; + responses: { + /** Existed backups */ + 200: { + schema: definitions['BackupListResponse']; + }; + /** Unauthorized or invalid credentials. */ + 401: unknown; + /** Forbidden */ + 403: { + schema: definitions['ErrorResponse']; + }; + /** Invalid backup list. */ + 422: { + schema: definitions['ErrorResponse']; + }; + /** An error has occurred while trying to fulfill the request. Most likely the ErrorResponse will contain more information about the error. */ + 500: { + schema: definitions['ErrorResponse']; + }; + }; + }; /** Starts a process of creating a backup for a set of classes */ 'backups.create': { parameters: { @@ -2752,6 +2804,35 @@ export interface operations { }; }; }; + /** Cancel created backup with specified ID */ + 'backups.cancel': { + parameters: { + path: { + /** Backup backend name e.g. filesystem, gcs, s3. */ + backend: string; + /** The ID of a backup. Must be URL-safe and work as a filesystem path, only lowercase, numbers, underscore, minus characters allowed. */ + id: string; + }; + }; + responses: { + /** Successfully deleted. */ + 204: never; + /** Unauthorized or invalid credentials. */ + 401: unknown; + /** Forbidden */ + 403: { + schema: definitions['ErrorResponse']; + }; + /** Invalid backup cancellation attempt. */ + 422: { + schema: definitions['ErrorResponse']; + }; + /** An error has occurred while trying to fulfill the request. Most likely the ErrorResponse will contain more information about the error. */ + 500: { + schema: definitions['ErrorResponse']; + }; + }; + }; /** Returns status of a backup restoration attempt for a set of classes */ 'backups.restore.status': { parameters: {