diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 306d14cb..2367244f 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -54,9 +54,7 @@ jobs: [ { runner: 'npm', jest_env: 'node' }, { runner: 'npm', jest_env: 'edge' }, - { runner: 'bun', jest_env: 'node', bun_version: '1.0.0' }, - { runner: 'bun', jest_env: 'node', bun_version: '1.0.36' }, - { runner: 'bun', jest_env: 'node', bun_version: '1.1.11' }, + { runner: 'bun', jest_env: 'node', bun_version: '1.2' }, ] steps: diff --git a/README.md b/README.md index d00bc12b..b204ffd9 100644 --- a/README.md +++ b/README.md @@ -640,6 +640,225 @@ const list = await pc.listCollections(); // } ``` +## Backups + +A backup is a static copy of a serverless index that only consumes storage. It is a non-queryable representation of a set of records. You can create a backup of a serverless index, and you can create a new serverless index from a backup. You can optionally apply new `tags` and `deletionProtection` configurations to the index. You can read more about [backups here](https://docs.pinecone.io/guides/manage-data/backups-overview). + +### Create a backup + +You can create a new backup from an existing index using the index name: + +```typescript +import { Pinecone } from '@pinecone-database/pinecone'; +const pc = new Pinecone(); +const backup = await pc.createBackup({ + indexName: 'my-index', + name: 'my-index-backup-1', + description: 'weekly backup', +}); +console.log(backup); +// { +// backupId: '11450b9f-96e5-47e5-9186-03f346b1f385', +// sourceIndexName: 'my-index', +// sourceIndexId: 'b480770b-600d-4c4e-bf19-799c933ae2bf', +// name: 'my-index-backup-1', +// description: 'weekly backup', +// status: 'Initializing', +// cloud: 'aws', +// region: 'us-east-1', +// dimension: 1024, +// metric: 'cosine', +// recordCount: 500, +// namespaceCount: 4, +// sizeBytes: 78294, +// tags: {}, +// createdAt: '2025-05-07T03:11:11.722238160Z' +// } +``` + +### Create a new index from a backup + +You can restore a serverless index by creating a new index from a backup. Optionally, you can provide +new `tags` or `deletionProtection` values when restoring an index. Creating an index from a backup intiates +a new restore job, which can be used to view the progress of the index restoration through `describeRestoreJob`. + +```typescript +import { Pinecone } from '@pinecone-database/pinecone'; +const pc = new Pinecone(); +const response = await pc.createIndexFromBackup({ + backupId: '11450b9f-96e5-47e5-9186-03f346b1f385', + name: 'my-index-restore-1', +}); +console.log(response); +// { +// restoreJobId: '4d4c8693-10fd-4204-a57b-1e3e626fca07', +// indexId: 'deb7688b-9f21-4c16-8eb7-f0027abd27fe' +// } +``` + +### Describe and list backups + +You can use a `backupId` and the `describeBackup` method to describe a specific backup: + +```typescript +import { Pinecone } from '@pinecone-database/pinecone'; +const pc = new Pinecone(); +const backup = await pc.describeBackup('11450b9f-96e5-47e5-9186-03f346b1f385'); +console.log(backup); +// { +// backupId: '11450b9f-96e5-47e5-9186-03f346b1f385', +// sourceIndexName: 'my-index', +// sourceIndexId: 'b480770b-600d-4c4e-bf19-799c933ae2bf', +// name: 'my-index-backup-1', +// description: 'weekly backup', +// status: 'Initializing', +// cloud: 'aws', +// region: 'us-east-1', +// dimension: 1024, +// metric: 'cosine', +// recordCount: 500, +// namespaceCount: 4, +// sizeBytes: 78294, +// tags: {}, +// createdAt: '2025-05-07T03:11:11.722238160Z' +// } +``` + +`listBackups` lists all the backups for a specific index, or your entire project. If an `indexName` is provided, only +the backups for that index will be listed. + +```typescript +import { Pinecone } from '@pinecone-database/pinecone'; +const pc = new Pinecone(); + +// list backups for the entire project +const projectBackups = await pc.listBackups({ limit: 2 }); +// list backups for a specific index +const myIndexBackups = await pc.listBackups({ + indexName: 'my-index', + limit: 2, +}); +console.log(myIndexBackups); +// { +// data: [ +// { +// backupId: '6a00902c-d118-4ad3-931c-49328c26d558', +// sourceIndexName: 'my-index', +// sourceIndexId: '0888b4d9-0b7b-447e-a403-ab057ceee4d4', +// name: 'my-index-backup-2', +// description: undefined, +// status: 'Ready', +// cloud: 'aws', +// region: 'us-east-1', +// dimension: 5, +// metric: 'cosine', +// recordCount: 200, +// namespaceCount: 2, +// sizeBytes: 67284, +// tags: {}, +// createdAt: '2025-05-07T18:34:13.626650Z' +// }, +// { +// backupId: '2b362ea3-b7cf-4950-866f-0dff37ab781e', +// sourceIndexName: 'my-index', +// sourceIndexId: '0888b4d9-0b7b-447e-a403-ab057ceee4d4', +// name: 'my-index-backup-1', +// description: undefined, +// status: 'Ready', +// cloud: 'aws', +// region: 'us-east-1', +// dimension: 1024, +// metric: 'cosine', +// recordCount: 500, +// namespaceCount: 4, +// sizeBytes: 78294, +// tags: {}, +// createdAt: '2025-05-07T18:33:59.888270Z' +// }, +// ], +// pagination: undefined +// } +``` + +### Describe and list restore jobs + +You can use a `restoreJobId` and the `describeRestoreJob` method to describe a specific backup: + +```typescript +import { Pinecone } from '@pinecone-database/pinecone'; +const pc = new Pinecone(); + +const restoreJob = await pc.describeRestoreJob( + '4d4c8693-10fd-4204-a57b-1e3e626fca07' +); +console.log(restoreJob); +// { +// restoreJobId: '4d4c8693-10fd-4204-a57b-1e3e626fca07', +// backupId: '11450b9f-96e5-47e5-9186-03f346b1f385', +// targetIndexName: 'my-index-restore-1', +// targetIndexId: 'deb7688b-9f21-4c16-8eb7-f0027abd27fe', +// status: 'Completed', +// createdAt: 2025-05-07T03:38:37.107Z, +// completedAt: 2025-05-07T03:40:23.687Z, +// percentComplete: 100 +// } +``` + +`listRestoreJobs` lists all the restore jobs for your project. + +```typescript +import { Pinecone } from '@pinecone-database/pinecone'; +const pc = new Pinecone(); + +const projectRestoreJobs = await pc.listRestoreJobs({ limit: 3 }); +console.log(projectRestoreJobs); +// { +// data: [ +// { +// restoreJobId: '4d4c8693-10fd-4204-a57b-1e3e626fca07', +// backupId: '11450b9f-96e5-47e5-9186-03f346b1f385', +// targetIndexName: 'my-index-restore-1', +// targetIndexId: 'deb7688b-9f21-4c16-8eb7-f0027abd27fe', +// status: 'Completed', +// createdAt: 2025-05-07T03:38:37.107Z, +// completedAt: 2025-05-07T03:40:23.687Z, +// percentComplete: 100 +// }, +// { +// restoreJobId: 'c60a62e0-63b9-452a-88af-31d89c56c988', +// backupId: '11450b9f-96e5-47e5-9186-03f346b1f385', +// targetIndexName: 'my-index-restore-2', +// targetIndexId: 'f2c9a846-799f-4b19-81a4-f3096b3d6114', +// status: 'Completed', +// createdAt: 2025-05-07T21:42:38.971Z, +// completedAt: 2025-05-07T21:43:11.782Z, +// percentComplete: 100 +// }, +// { +// restoreJobId: '792837b7-8001-47bf-9c11-1859826b9c10', +// backupId: '11450b9f-96e5-47e5-9186-03f346b1f385', +// targetIndexName: 'my-index-restore-3', +// targetIndexId: '620dda62-c999-4dd1-b083-6beb087b31e7', +// status: 'Pending', +// createdAt: 2025-05-07T21:48:39.580Z, +// completedAt: 2025-05-07T21:49:12.084Z, +// percentComplete: 45 +// } +// ], +// pagination: undefined +// } +``` + +### Delete backups + +You can delete a backup using the backupId and `deleteBackup`. + +```typescript +import { Pinecone } from '@pinecone-database/pinecone'; +const pc = new Pinecone(); +await pc.deleteBackup('6a00902c-d118-4ad3-931c-49328c26d558'); +``` + ## Index operations Pinecone indexes support operations for working with vector data using methods such as upsert, query, fetch, and delete. diff --git a/src/control/__tests__/createBackup.test.ts b/src/control/__tests__/createBackup.test.ts new file mode 100644 index 00000000..2862c861 --- /dev/null +++ b/src/control/__tests__/createBackup.test.ts @@ -0,0 +1,62 @@ +import { createBackup } from '../createBackup'; +import { ManageIndexesApi } from '../../pinecone-generated-ts-fetch/db_control'; +import type { + BackupModel, + CreateBackupOperationRequest, +} from '../../pinecone-generated-ts-fetch/db_control'; +import { PineconeArgumentError } from '../../errors'; + +const setupCreateBackupResponse = ( + createBackupResponse = {} as BackupModel, + isCreateBackupSuccess = true +) => { + const fakeCreateBackup: ( + req: CreateBackupOperationRequest + ) => Promise = jest + .fn() + .mockImplementation(() => + isCreateBackupSuccess + ? Promise.resolve(createBackupResponse) + : Promise.reject(createBackupResponse) + ); + + const MIA = { + createBackup: fakeCreateBackup, + } as ManageIndexesApi; + + return MIA; +}; + +describe('createBackup', () => { + test('calls the openapi create backup endpoint, passing name and description', async () => { + const MIA = setupCreateBackupResponse(); + const returned = await createBackup(MIA)({ + indexName: 'index-name', + name: 'backup-name', + description: 'backup-description', + }); + expect(returned).toEqual({} as BackupModel); + expect(MIA.createBackup).toHaveBeenCalledWith({ + indexName: 'index-name', + createBackupRequest: { + name: 'backup-name', + description: 'backup-description', + }, + }); + }); + + test('throws an error if indexName is not provided', async () => { + const MIA = setupCreateBackupResponse(); + await expect( + createBackup(MIA)({ + indexName: '', + name: 'backup-name', + description: 'backup-description', + }) + ).rejects.toThrow( + new PineconeArgumentError( + 'You must pass a non-empty string for `indexName` in order to create a backup' + ) + ); + }); +}); diff --git a/src/control/__tests__/createIndexFromBackup.test.ts b/src/control/__tests__/createIndexFromBackup.test.ts new file mode 100644 index 00000000..cab33693 --- /dev/null +++ b/src/control/__tests__/createIndexFromBackup.test.ts @@ -0,0 +1,74 @@ +import { createIndexFromBackup } from '../createIndexFromBackup'; +import { ManageIndexesApi } from '../../pinecone-generated-ts-fetch/db_control'; +import type { + BackupModel, + CreateIndexFromBackupOperationRequest, + CreateIndexFromBackupResponse, +} from '../../pinecone-generated-ts-fetch/db_control'; +import { PineconeArgumentError } from '../../errors'; + +const setupCreateIndexFromBackupResponse = ( + createIndexFromBackupResponse = {} as BackupModel, + isCreateIndexFromBackupSuccess = true +) => { + const fakeCreateIndexFromBackup: ( + req: CreateIndexFromBackupOperationRequest + ) => Promise = jest + .fn() + .mockImplementation(() => + isCreateIndexFromBackupSuccess + ? Promise.resolve(createIndexFromBackupResponse) + : Promise.reject(createIndexFromBackupResponse) + ); + + const MIA = { + createIndexFromBackup: fakeCreateIndexFromBackup, + } as ManageIndexesApi; + + return MIA; +}; + +describe('createIndexFromBackup', () => { + test('calls the openapi create indexn from backup endpoint, passing backupId, name, tags, and deletionProtection', async () => { + const MIA = setupCreateIndexFromBackupResponse(); + const returned = await createIndexFromBackup(MIA)({ + backupId: '12345-ajfielkas-123123', + name: 'my-restored-index', + tags: { test: 'test-tag' }, + deletionProtection: 'enabled', + }); + expect(returned).toEqual({} as CreateIndexFromBackupResponse); + expect(MIA.createIndexFromBackup).toHaveBeenCalledWith({ + backupId: '12345-ajfielkas-123123', + createIndexFromBackupRequest: { + name: 'my-restored-index', + tags: { test: 'test-tag' }, + deletionProtection: 'enabled', + }, + }); + }); + + test('throws an error if backupId or name are not provided', async () => { + const MIA = setupCreateIndexFromBackupResponse(); + await expect( + createIndexFromBackup(MIA)({ + backupId: '', + name: 'my-restored-index', + }) + ).rejects.toThrow( + new PineconeArgumentError( + 'You must pass a non-empty string for `backupId` in order to create an index from backup' + ) + ); + await expect( + createIndexFromBackup(MIA)({ + backupId: '123-123-123-123', + name: '', + }) + ).rejects.toThrow( + new PineconeArgumentError( + 'You must pass a non-empty string for `name` in order to create an index from backup' + ) + ); + }); +}); diff --git a/src/control/__tests__/deleteBackup.test.ts b/src/control/__tests__/deleteBackup.test.ts new file mode 100644 index 00000000..2ca84df5 --- /dev/null +++ b/src/control/__tests__/deleteBackup.test.ts @@ -0,0 +1,37 @@ +import { deleteBackup } from '../deleteBackup'; +import { PineconeArgumentError } from '../../errors'; +import { + DeleteBackupRequest, + ManageIndexesApi, +} from '../../pinecone-generated-ts-fetch/db_control'; + +describe('deleteBackup', () => { + const setupSuccessResponse = (responseData) => { + const fakeDeleteBackup: (req: DeleteBackupRequest) => Promise = jest + .fn() + .mockImplementation(() => Promise.resolve(responseData)); + const MIA = { + deleteBackup: fakeDeleteBackup, + } as ManageIndexesApi; + + return MIA; + }; + + test('calls the openapi delete backup endpoint, passing backupId', async () => { + const MIA = setupSuccessResponse(undefined); + const returned = await deleteBackup(MIA)('backup-id'); + expect(returned).toEqual(undefined); + expect(MIA.deleteBackup).toHaveBeenCalledWith({ + backupId: 'backup-id', + }); + }); + + test('should throw backupId is not provided', async () => { + const MIA = setupSuccessResponse(''); + // @ts-ignore + await expect(deleteBackup(MIA)()).rejects.toThrow( + 'You must pass a non-empty string for `backupId` in order to delete a backup' + ); + await expect(deleteBackup(MIA)('')).rejects.toThrow(PineconeArgumentError); + }); +}); diff --git a/src/control/__tests__/describeBackup.test.ts b/src/control/__tests__/describeBackup.test.ts new file mode 100644 index 00000000..e9d68086 --- /dev/null +++ b/src/control/__tests__/describeBackup.test.ts @@ -0,0 +1,42 @@ +import { describeBackup } from '../describeBackup'; +import { PineconeArgumentError } from '../../errors'; +import { + BackupModel, + DescribeBackupRequest, + ManageIndexesApi, +} from '../../pinecone-generated-ts-fetch/db_control'; + +describe('describeBackup', () => { + const setupSuccessResponse = (responseData) => { + const fakeDescribeBackup: ( + req: DescribeBackupRequest + ) => Promise = jest + .fn() + .mockImplementation(() => Promise.resolve(responseData)); + const MIA = { + describeBackup: fakeDescribeBackup, + } as ManageIndexesApi; + + return MIA; + }; + + test('calls the openapi describe backup endpoint, passing backupId', async () => { + const MIA = setupSuccessResponse(undefined); + const returned = await describeBackup(MIA)('backup-id'); + expect(returned).toEqual(undefined); + expect(MIA.describeBackup).toHaveBeenCalledWith({ + backupId: 'backup-id', + }); + }); + + test('should throw backupId is not provided', async () => { + const MIA = setupSuccessResponse(''); + // @ts-ignore + await expect(describeBackup(MIA)()).rejects.toThrow( + 'You must pass a non-empty string for `backupId` in order to describe a backup' + ); + await expect(describeBackup(MIA)('')).rejects.toThrow( + PineconeArgumentError + ); + }); +}); diff --git a/src/control/__tests__/describeRestoreJob.test.ts b/src/control/__tests__/describeRestoreJob.test.ts new file mode 100644 index 00000000..a6c017e4 --- /dev/null +++ b/src/control/__tests__/describeRestoreJob.test.ts @@ -0,0 +1,42 @@ +import { describeRestoreJob } from '../describeRestoreJob'; +import { PineconeArgumentError } from '../../errors'; +import { + DescribeRestoreJobRequest, + ManageIndexesApi, + RestoreJobModel, +} from '../../pinecone-generated-ts-fetch/db_control'; + +describe('describeRestoreJob', () => { + const setupSuccessResponse = (responseData) => { + const fakeDescribeRestoreJob: ( + req: DescribeRestoreJobRequest + ) => Promise = jest + .fn() + .mockImplementation(() => Promise.resolve(responseData)); + const MIA = { + describeRestoreJob: fakeDescribeRestoreJob, + } as ManageIndexesApi; + + return MIA; + }; + + test('calls the openapi describe restore job endpoint, passing jobId', async () => { + const MIA = setupSuccessResponse(undefined); + const returned = await describeRestoreJob(MIA)('restore-job-id'); + expect(returned).toEqual(undefined); + expect(MIA.describeRestoreJob).toHaveBeenCalledWith({ + jobId: 'restore-job-id', + }); + }); + + test('should throw backupId is not provided', async () => { + const MIA = setupSuccessResponse(''); + // @ts-ignore + await expect(describeRestoreJob(MIA)()).rejects.toThrow( + 'You must pass a non-empty string for `restoreJobId` in order to describe a restore job' + ); + await expect(describeRestoreJob(MIA)('')).rejects.toThrow( + PineconeArgumentError + ); + }); +}); diff --git a/src/control/__tests__/listBackups.test.ts b/src/control/__tests__/listBackups.test.ts new file mode 100644 index 00000000..ce3e82d3 --- /dev/null +++ b/src/control/__tests__/listBackups.test.ts @@ -0,0 +1,47 @@ +import { listBackups } from '../listBackups'; +import { + BackupList, + ListIndexBackupsRequest, + ManageIndexesApi, +} from '../../pinecone-generated-ts-fetch/db_control'; + +describe('listBackups', () => { + const setupSuccessResponse = (responseData = {}) => { + const fakeListIndexBackups: ( + req: ListIndexBackupsRequest + ) => Promise = jest + .fn() + .mockImplementation(() => Promise.resolve(responseData)); + + const fakeListProjectBackups: () => Promise = jest + .fn() + .mockImplementation(() => Promise.resolve(responseData)); + + const MIA = { + listIndexBackups: fakeListIndexBackups, + listProjectBackups: fakeListProjectBackups, + } as ManageIndexesApi; + + return MIA; + }; + + test('calls the openapi describe index backup endpoint when indexName provided', async () => { + const MIA = setupSuccessResponse(); + await listBackups(MIA)({ + indexName: 'my-index', + limit: 10, + paginationToken: 'pagination-token', + }); + expect(MIA.listIndexBackups).toHaveBeenCalledWith({ + indexName: 'my-index', + limit: 10, + paginationToken: 'pagination-token', + }); + }); + + test('calls the openapi describe project backup endpoint when indexName is not provided', async () => { + const MIA = setupSuccessResponse(undefined); + await listBackups(MIA)(); + expect(MIA.listProjectBackups).toHaveBeenCalled(); + }); +}); diff --git a/src/control/__tests__/listRestoreJobs.test.ts b/src/control/__tests__/listRestoreJobs.test.ts new file mode 100644 index 00000000..f7fb7de9 --- /dev/null +++ b/src/control/__tests__/listRestoreJobs.test.ts @@ -0,0 +1,34 @@ +import { listRestoreJobs } from '../listRestoreJobs'; +import { + RestoreJobList, + ManageIndexesApi, + ListRestoreJobsRequest, +} from '../../pinecone-generated-ts-fetch/db_control'; + +describe('listBackups', () => { + const setupSuccessResponse = (responseData = {}) => { + const fakeListRestoreJobs: ( + req: ListRestoreJobsRequest + ) => Promise = jest + .fn() + .mockImplementation(() => Promise.resolve(responseData)); + + const MIA = { + listRestoreJobs: fakeListRestoreJobs, + } as ManageIndexesApi; + + return MIA; + }; + + test('calls the openapi describe index backup endpoint when indexName provided', async () => { + const MIA = setupSuccessResponse(); + await listRestoreJobs(MIA)({ + limit: 10, + paginationToken: 'pagination-token', + }); + expect(MIA.listRestoreJobs).toHaveBeenCalledWith({ + limit: 10, + paginationToken: 'pagination-token', + }); + }); +}); diff --git a/src/control/createBackup.ts b/src/control/createBackup.ts new file mode 100644 index 00000000..69a3a76a --- /dev/null +++ b/src/control/createBackup.ts @@ -0,0 +1,42 @@ +import { + BackupModel, + ManageIndexesApi, +} from '../pinecone-generated-ts-fetch/db_control'; + +/** + * The options for creating an index backup. + */ +export interface CreateBackupOptions { + /** + * The name of the index to back up. + */ + indexName: string; + /** + * An optional name for the backup. If not provided, one will be auto-generated. + */ + name?: string; + /** + * A human-readable description of the backup's purpose or contents. + */ + description?: string; +} + +export const createBackup = (api: ManageIndexesApi) => { + return async ( + createBackupOptions: CreateBackupOptions + ): Promise => { + if (!createBackupOptions.indexName) { + throw new Error( + 'You must pass a non-empty string for `indexName` in order to create a backup' + ); + } + + return await api.createBackup({ + indexName: createBackupOptions.indexName, + createBackupRequest: { + name: createBackupOptions.name, + description: createBackupOptions.description, + }, + }); + }; +}; diff --git a/src/control/createIndexFromBackup.ts b/src/control/createIndexFromBackup.ts new file mode 100644 index 00000000..2eedba06 --- /dev/null +++ b/src/control/createIndexFromBackup.ts @@ -0,0 +1,54 @@ +import { + CreateIndexFromBackupResponse, + DeletionProtection, + ManageIndexesApi, +} from '../pinecone-generated-ts-fetch/db_control'; + +/** + * The options for creating a new index from an existing backup. + */ +export interface CreateIndexFromBackupOptions { + /** + * The ID of the backup to restore from. + */ + backupId: string; + /** + * The name of the new index to create from the backup. Resource name must be 1-45 characters long, start and end with an alphanumeric character, and consist only of lower case alphanumeric characters or '-'. + */ + name: string; + /** + * Optional custom user tags to attach to the restored index. Keys must be 80 characters or less. Values must be 120 characters or less. + * Keys must be alphanumeric, '_', or '-'. Values must be alphanumeric, ';', '@', '_', '-', '.', '+', or ' '. + * To unset a key, set the value to be an empty string. + */ + tags?: { [key: string]: string }; + /** + * Allows configuring deletion protection for the new index: 'enabled' or 'disabled'. Defaults to 'disabled'. + */ + deletionProtection?: DeletionProtection; +} + +export const createIndexFromBackup = (api: ManageIndexesApi) => { + return async ( + createIndexFromBackupOptions: CreateIndexFromBackupOptions + ): Promise => { + if (!createIndexFromBackupOptions.backupId) { + throw new Error( + 'You must pass a non-empty string for `backupId` in order to create an index from backup' + ); + } else if (!createIndexFromBackupOptions.name) { + throw new Error( + 'You must pass a non-empty string for `name` in order to create an index from backup' + ); + } + + return await api.createIndexFromBackup({ + backupId: createIndexFromBackupOptions.backupId, + createIndexFromBackupRequest: { + name: createIndexFromBackupOptions.name, + tags: createIndexFromBackupOptions.tags, + deletionProtection: createIndexFromBackupOptions.deletionProtection, + }, + }); + }; +}; diff --git a/src/control/deleteBackup.ts b/src/control/deleteBackup.ts new file mode 100644 index 00000000..42c66030 --- /dev/null +++ b/src/control/deleteBackup.ts @@ -0,0 +1,20 @@ +import { ManageIndexesApi } from '../pinecone-generated-ts-fetch/db_control'; +import type { BackupId } from './types'; +import { PineconeArgumentError } from '../errors'; + +/** + * The string ID of the backup to delete. + */ +export type DeleteBackupOptions = BackupId; + +export const deleteBackup = (api: ManageIndexesApi) => { + return async (backupId: DeleteBackupOptions): Promise => { + if (!backupId) { + throw new PineconeArgumentError( + 'You must pass a non-empty string for `backupId` in order to delete a backup' + ); + } + + return await api.deleteBackup({ backupId: backupId }); + }; +}; diff --git a/src/control/describeBackup.ts b/src/control/describeBackup.ts new file mode 100644 index 00000000..fc1e3af5 --- /dev/null +++ b/src/control/describeBackup.ts @@ -0,0 +1,23 @@ +import { + ManageIndexesApi, + BackupModel, +} from '../pinecone-generated-ts-fetch/db_control'; +import type { BackupId } from './types'; +import { PineconeArgumentError } from '../errors'; + +/** + * The string ID of the backup to describe. + */ +export type DescribeBackupOptions = BackupId; + +export const describeBackup = (api: ManageIndexesApi) => { + return async (backupId: DescribeBackupOptions): Promise => { + if (!backupId) { + throw new PineconeArgumentError( + 'You must pass a non-empty string for `backupId` in order to describe a backup' + ); + } + + return await api.describeBackup({ backupId: backupId }); + }; +}; diff --git a/src/control/describeRestoreJob.ts b/src/control/describeRestoreJob.ts new file mode 100644 index 00000000..890d5a0a --- /dev/null +++ b/src/control/describeRestoreJob.ts @@ -0,0 +1,25 @@ +import { + ManageIndexesApi, + RestoreJobModel, +} from '../pinecone-generated-ts-fetch/db_control'; +import type { RestoreJobId } from './types'; +import { PineconeArgumentError } from '../errors'; + +/** + * The string ID of the restore job to describe. + */ +export type DescribeRestoreJobOptions = RestoreJobId; + +export const describeRestoreJob = (api: ManageIndexesApi) => { + return async ( + restoreJobId: DescribeRestoreJobOptions + ): Promise => { + if (!restoreJobId) { + throw new PineconeArgumentError( + 'You must pass a non-empty string for `restoreJobId` in order to describe a restore job' + ); + } + + return await api.describeRestoreJob({ jobId: restoreJobId }); + }; +}; diff --git a/src/control/index.ts b/src/control/index.ts index 63061242..1efd82f5 100644 --- a/src/control/index.ts +++ b/src/control/index.ts @@ -28,3 +28,19 @@ export type { DeleteCollectionOptions } from './deleteCollection'; export { describeCollection } from './describeCollection'; export type { DescribeCollectionOptions } from './describeCollection'; export { listCollections } from './listCollections'; + +// Backup Operations +export { createBackup } from './createBackup'; +export type { CreateBackupOptions } from './createBackup'; +export { createIndexFromBackup } from './createIndexFromBackup'; +export type { CreateIndexFromBackupOptions } from './createIndexFromBackup'; +export { describeBackup } from './describeBackup'; +export type { DescribeBackupOptions } from './describeBackup'; +export { describeRestoreJob } from './describeRestoreJob'; +export type { DescribeRestoreJobOptions } from './describeRestoreJob'; +export { listBackups } from './listBackups'; +export type { ListBackupsOptions } from './listBackups'; +export { listRestoreJobs } from './listRestoreJobs'; +export type { ListRestoreJobsOptions } from './listRestoreJobs'; +export { deleteBackup } from './deleteBackup'; +export type { DeleteBackupOptions } from './deleteBackup'; diff --git a/src/control/listBackups.ts b/src/control/listBackups.ts new file mode 100644 index 00000000..2c609dac --- /dev/null +++ b/src/control/listBackups.ts @@ -0,0 +1,35 @@ +import { + ManageIndexesApi, + BackupList, +} from '../pinecone-generated-ts-fetch/db_control'; + +/** + * The options for listing backups. + */ +export interface ListBackupsOptions { + /** + * The index name to list backups for. If not provided, all project backups will be listed. + */ + indexName?: string; + /** + * Maximum number of backups to return. + */ + limit?: number; + /** + * Token used for pagination to retrieve the next page of results. + */ + paginationToken?: string; +} + +export const listBackups = (api: ManageIndexesApi) => { + return async ( + listBackupOptions: ListBackupsOptions = {} + ): Promise => { + const { indexName, ...rest } = listBackupOptions; + if (!indexName) { + return await api.listProjectBackups(); + } else { + return await api.listIndexBackups({ indexName, ...rest }); + } + }; +}; diff --git a/src/control/listRestoreJobs.ts b/src/control/listRestoreJobs.ts new file mode 100644 index 00000000..ad0d8214 --- /dev/null +++ b/src/control/listRestoreJobs.ts @@ -0,0 +1,26 @@ +import { + ManageIndexesApi, + RestoreJobList, +} from '../pinecone-generated-ts-fetch/db_control'; + +/** + * The options for listing restore jobs. + */ +export interface ListRestoreJobsOptions { + /** + * Maximum number of restore jobs to return. + */ + limit?: number; + /** + * Token used for pagination to retrieve the next page of results. + */ + paginationToken?: string; +} + +export const listRestoreJobs = (api: ManageIndexesApi) => { + return async ( + listBackupOptions: ListRestoreJobsOptions + ): Promise => { + return await api.listRestoreJobs(listBackupOptions); + }; +}; diff --git a/src/control/types.ts b/src/control/types.ts index b735b059..73379f43 100644 --- a/src/control/types.ts +++ b/src/control/types.ts @@ -20,6 +20,16 @@ export type IndexName = string; */ export type CollectionName = string; +/** + * The unique identifier representing a backup. + * + * @see [Backups overview](https://docs.pinecone.io/guides/manage-data/backups-overview) + */ +export type BackupId = string; + +/** The unique identifier representing a restore job. */ +export type RestoreJobId = string; + /** * @see [Understanding indexes](https://docs.pinecone.io/docs/indexes) */ diff --git a/src/index.ts b/src/index.ts index ff73f4e8..74bfb4d9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -99,10 +99,14 @@ export type { ScoredPineconeRecord, } from './data'; export type { + BackupList, + BackupModel, CollectionList, CollectionModel, ConfigureIndexRequestSpecPod, CreateCollectionRequest, + CreateIndexForModelRequest, + CreateIndexFromBackupResponse, CreateIndexRequest, CreateIndexRequestMetricEnum, DeletionProtection, @@ -111,10 +115,11 @@ export type { FetchAPI, IndexList, IndexModel, - ServerlessSpec, - ServerlessSpecCloudEnum, + IndexModelMetricEnum, PodSpec, PodSpecMetadataConfig, - CreateIndexForModelRequest, - IndexModelMetricEnum, + RestoreJobList, + RestoreJobModel, + ServerlessSpec, + ServerlessSpecCloudEnum, } from './pinecone-generated-ts-fetch/db_control'; diff --git a/src/integration/data/namespaces/namespaces.test.ts b/src/integration/data/namespaces/namespaces.test.ts index 6b0ade83..5bab3d46 100644 --- a/src/integration/data/namespaces/namespaces.test.ts +++ b/src/integration/data/namespaces/namespaces.test.ts @@ -1,5 +1,5 @@ -import { Pinecone } from '../../../index'; -import { generateRecords, sleep } from '../../test-helpers'; +import { ListNamespacesResponse, Pinecone } from '../../../index'; +import { assertWithRetries, generateRecords, sleep } from '../../test-helpers'; const namespaceOne = 'namespace-one'; const namespaceTwo = 'namespace-two'; @@ -53,14 +53,21 @@ describe('namespaces operations', () => { test('delete namespace', async () => { await pinecone.index(serverlessIndexName).deleteNamespace(namespaceTwo); - await sleep(2000); // Wait for the delete operation to complete - const response = await pinecone.index(serverlessIndexName).listNamespaces(); - expect(response.namespaces).toEqual( - expect.arrayContaining([expect.objectContaining({ name: namespaceOne })]) - ); - expect(response.namespaces).not.toEqual( - expect.arrayContaining([expect.objectContaining({ name: namespaceTwo })]) + await assertWithRetries( + () => pinecone.index(serverlessIndexName).listNamespaces(), + (response: ListNamespacesResponse) => { + expect(response.namespaces).toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: namespaceOne }), + ]) + ); + expect(response.namespaces).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: namespaceTwo }), + ]) + ); + } ); }); }); diff --git a/src/integration/test-helpers.ts b/src/integration/test-helpers.ts index c23fb677..33f2af85 100644 --- a/src/integration/test-helpers.ts +++ b/src/integration/test-helpers.ts @@ -158,7 +158,7 @@ type Assertions = (result: any) => void; export const assertWithRetries = async ( asyncFn: () => Promise, assertionsFn: Assertions, - totalMsWait: number = 90000, + totalMsWait: number = 180000, delay: number = 3000 ) => { let lastError: any = null; diff --git a/src/pinecone.ts b/src/pinecone.ts index c6e1b633..766a4778 100644 --- a/src/pinecone.ts +++ b/src/pinecone.ts @@ -1,19 +1,33 @@ import { - describeIndex, - listIndexes, + configureIndex, + createBackup, + createCollection, createIndex, createIndexForModel, + createIndexFromBackup, + deleteBackup, + deleteCollection, deleteIndex, - configureIndex, - listCollections, - createCollection, + describeBackup, describeCollection, - deleteCollection, - CreateIndexOptions, - IndexName, + describeIndex, + describeRestoreJob, indexOperationsBuilder, + listBackups, + listCollections, + listIndexes, + listRestoreJobs, CollectionName, + CreateBackupOptions, + CreateIndexFromBackupOptions, + DeleteBackupOptions, + DescribeBackupOptions, + ListBackupsOptions, + ListRestoreJobsOptions, CreateIndexForModelOptions, + CreateIndexOptions, + IndexName, + DescribeRestoreJobOptions, } from './control'; import { createAssistant, @@ -118,6 +132,20 @@ export class Pinecone { private _describeAssistant: ReturnType; /** @hidden */ private _listAssistants: ReturnType; + /** @hidden */ + private _createBackup: ReturnType; + /** @hidden */ + private _createIndexFromBackup: ReturnType; + /** @hidden */ + private _describeBackup: ReturnType; + /** @hidden */ + private _describeRestoreJob: ReturnType; + /** @hidden */ + private _deleteBackup: ReturnType; + /** @hidden */ + private _listBackups: ReturnType; + /** @hidden */ + private _listRestoreJobs: ReturnType; public inference: Inference; @@ -172,6 +200,14 @@ export class Pinecone { this._describeAssistant = describeAssistant(asstControlApi); this._listAssistants = listAssistants(asstControlApi); + this._createBackup = createBackup(api); + this._createIndexFromBackup = createIndexFromBackup(api); + this._describeBackup = describeBackup(api); + this._describeRestoreJob = describeRestoreJob(api); + this._deleteBackup = deleteBackup(api); + this._listBackups = listBackups(api); + this._listRestoreJobs = listRestoreJobs(api); + this.inference = new Inference(infApi); } @@ -784,7 +820,7 @@ export class Pinecone { * ``` * * @param assistantName - The name of the assistant being updated. - * @param options - An {@link updateAssistant} object containing the name of the assistant to be updated and + * @param options - An {@link UpdateAssistantOptions} object containing the name of the assistant to be updated and * optional instructions and metadata. * @throws Error if the Assistant API is not initialized. * @returns A Promise that resolves to an {@link UpdateAssistant200Response} object. @@ -809,6 +845,270 @@ export class Pinecone { return this.config; } + /** + * Creates a backup of an index. + * + * @example + * ```typescript + * import { Pinecone } from '@pinecone-database/pinecone'; + * const pc = new Pinecone(); + * const backup = await pc.createBackup({ indexName: 'my-index', name: 'my-index-backup-1', description: 'weekly backup' }); + * console.log(backup); + * // { + * // backupId: '11450b9f-96e5-47e5-9186-03f346b1f385', + * // sourceIndexName: 'my-index', + * // sourceIndexId: 'b480770b-600d-4c4e-bf19-799c933ae2bf', + * // name: 'my-index-backup-1', + * // description: 'weekly backup', + * // status: 'Initializing', + * // cloud: 'aws', + * // region: 'us-east-1', + * // dimension: 1024, + * // metric: 'cosine', + * // recordCount: 500, + * // namespaceCount: 4, + * // sizeBytes: 78294, + * // tags: {}, + * // createdAt: '2025-05-07T03:11:11.722238160Z' + * // } + * ``` + * + * @param options - A {@link CreateBackupOptions} object containing the indexName to backup, and an optional name + * and description for the backup. + * @throws {@link Errors.PineconeArgumentError} when arguments passed to the method fail a runtime validation. + * @throws {@link Errors.PineconeConnectionError} when network problems or an outage of Pinecone's APIs prevent the request from being completed. + * @returns A Promise that resolves to a {@link BackupModel} object. + */ + createBackup(options: CreateBackupOptions) { + return this._createBackup(options); + } + + /** + * Creates an index from an existing backup. + * + * @example + * ```typescript + * import { Pinecone } from '@pinecone-database/pinecone'; + * const pc = new Pinecone(); + * const response = await pc.createIndexFromBackup({ backupId: '11450b9f-96e5-47e5-9186-03f346b1f385', name: 'my-index-restore-1' }); + * console.log(response); + * // { + * // restoreJobId: '4d4c8693-10fd-4204-a57b-1e3e626fca07', + * // indexId: 'deb7688b-9f21-4c16-8eb7-f0027abd27fe' + * // } + * ``` + * + * @param options - A {@link CreateIndexFromBackupOptions} object containing the backupId for the backup to restore + * the index from, and the name of the new index. Optionally, you can provide new tags or deletionProtection values for the index. + * @throws {@link Errors.PineconeArgumentError} when arguments passed to the method fail a runtime validation. + * @throws {@link Errors.PineconeConnectionError} when network problems or an outage of Pinecone's APIs prevent the request from being completed. + * @returns A Promise that resolves to a {@link CreateIndexFromBackupResponse} object. + */ + createIndexFromBackup(options: CreateIndexFromBackupOptions) { + return this._createIndexFromBackup(options); + } + + /** + * Describes a backup. + * + * @example + * ```typescript + * import { Pinecone } from '@pinecone-database/pinecone'; + * const pc = new Pinecone(); + * const backup = await pc.describeBackup('11450b9f-96e5-47e5-9186-03f346b1f385'); + * console.log(backup); + * // { + * // backupId: '11450b9f-96e5-47e5-9186-03f346b1f385', + * // sourceIndexName: 'my-index', + * // sourceIndexId: 'b480770b-600d-4c4e-bf19-799c933ae2bf', + * // name: 'my-index-backup-1', + * // description: 'weekly backup', + * // status: 'Initializing', + * // cloud: 'aws', + * // region: 'us-east-1', + * // dimension: 1024, + * // metric: 'cosine', + * // recordCount: 500, + * // namespaceCount: 4, + * // sizeBytes: 78294, + * // tags: {}, + * // createdAt: '2025-05-07T03:11:11.722238160Z' + * // } + * ``` + * + * @param options - The backupId of the backup to describe. + * @throws {@link Errors.PineconeArgumentError} when arguments passed to the method fail a runtime validation. + * @throws {@link Errors.PineconeConnectionError} when network problems or an outage of Pinecone's APIs prevent the request from being completed. + * @returns A Promise that resolves to a {@link BackupModel} object. + */ + describeBackup(backupName: DescribeBackupOptions) { + return this._describeBackup(backupName); + } + + /** + * Describes a restore job. + * + * @example + * ```typescript + * import { Pinecone } from '@pinecone-database/pinecone'; + * const pc = new Pinecone(); + * const restoreJob = await pc.describeRestoreJob('4d4c8693-10fd-4204-a57b-1e3e626fca07'); + * console.log(restoreJob); + * // { + * // restoreJobId: '4d4c8693-10fd-4204-a57b-1e3e626fca07', + * // backupId: '11450b9f-96e5-47e5-9186-03f346b1f385', + * // targetIndexName: 'my-index-restore-1', + * // targetIndexId: 'deb7688b-9f21-4c16-8eb7-f0027abd27fe', + * // status: 'Completed', + * // createdAt: 2025-05-07T03:38:37.107Z, + * // completedAt: 2025-05-07T03:40:23.687Z, + * // percentComplete: 100 + * // } + * ``` + * + * @param options - The restoreJobId of the restore job to describe. + * @throws {@link Errors.PineconeArgumentError} when arguments passed to the method fail a runtime validation. + * @throws {@link Errors.PineconeConnectionError} when network problems or an outage of Pinecone's APIs prevent the request from being completed. + * @returns A Promise that resolves to a {@link RestoreJobModel} object. + */ + describeRestoreJob(restoreJobId: DescribeRestoreJobOptions) { + return this._describeRestoreJob(restoreJobId); + } + + /** + * Deletes a backup. + * + * @example + * ```typescript + * import { Pinecone } from '@pinecone-database/pinecone'; + * const pc = new Pinecone(); + * await pc.deleteBackup('11450b9f-96e5-47e5-9186-03f346b1f385'); + * ``` + * + * @param options - The backupId of the backup to delete. + * @throws {@link Errors.PineconeArgumentError} when arguments passed to the method fail a runtime validation. + * @throws {@link Errors.PineconeConnectionError} when network problems or an outage of Pinecone's APIs prevent the request from being completed. + * @returns A Promise that resolves when the request to delete the backup is completed. + */ + deleteBackup(backupName: DeleteBackupOptions) { + return this._deleteBackup(backupName); + } + + /** + * Lists backups within a project or a specific index. Pass an indexName to list backups for that index, + * otherwise the operation will return all backups in the project. + * + * @example + * ```typescript + * import { Pinecone } from '@pinecone-database/pinecone'; + * const pc = new Pinecone(); + * const backupsList = await pc.listBackups({ indexName: 'my-index', limit: 2 }); + * console.log(backupsList); + * // { + * // data: [ + * // { + * // backupId: '6a00902c-d118-4ad3-931c-49328c26d558', + * // sourceIndexName: 'my-index', + * // sourceIndexId: '0888b4d9-0b7b-447e-a403-ab057ceee4d4', + * // name: 'my-index-backup-2', + * // description: undefined, + * // status: 'Ready', + * // cloud: 'aws', + * // region: 'us-east-1', + * // dimension: 5, + * // metric: 'cosine', + * // recordCount: 200, + * // namespaceCount: 2, + * // sizeBytes: 67284, + * // tags: {}, + * // createdAt: '2025-05-07T18:34:13.626650Z' + * // }, + * // { + * // backupId: '2b362ea3-b7cf-4950-866f-0dff37ab781e', + * // sourceIndexName: 'my-index', + * // sourceIndexId: '0888b4d9-0b7b-447e-a403-ab057ceee4d4', + * // name: 'my-index-backup-1', + * // description: undefined, + * // status: 'Ready', + * // cloud: 'aws', + * // region: 'us-east-1', + * // dimension: 1024, + * // metric: 'cosine', + * // recordCount: 500, + * // namespaceCount: 4, + * // sizeBytes: 78294, + * // tags: {}, + * // createdAt: '2025-05-07T18:33:59.888270Z' + * // }, + * // ], + * // pagination: undefined + * // } + * ``` + * + * @param options - A {@link ListBackupsOptions} object containing the optional indexName, limit, and paginationToken values. + * @throws {@link Errors.PineconeArgumentError} when arguments passed to the method fail a runtime validation. + * @throws {@link Errors.PineconeConnectionError} when network problems or an outage of Pinecone's APIs prevent the request from being completed. + * @returns A Promise that resolves to a {@link BackupList} object. + */ + listBackups(options: ListBackupsOptions) { + return this._listBackups(options); + } + + /** + * Lists restore jobs within a project. + * + * @example + * ```typescript + * import { Pinecone } from '@pinecone-database/pinecone'; + * const pc = new Pinecone(); + * const restoreJobsList = await pc.listRestoreJobs({ limit: 3 }); + * console.log(restoreJobsList); + * // { + * // data: [ + * // { + * // restoreJobId: '4d4c8693-10fd-4204-a57b-1e3e626fca07', + * // backupId: '11450b9f-96e5-47e5-9186-03f346b1f385', + * // targetIndexName: 'my-index-restore-1', + * // targetIndexId: 'deb7688b-9f21-4c16-8eb7-f0027abd27fe', + * // status: 'Completed', + * // createdAt: 2025-05-07T03:38:37.107Z, + * // completedAt: 2025-05-07T03:40:23.687Z, + * // percentComplete: 100 + * // }, + * // { + * // restoreJobId: 'c60a62e0-63b9-452a-88af-31d89c56c988', + * // backupId: '11450b9f-96e5-47e5-9186-03f346b1f385', + * // targetIndexName: 'my-index-restore-2', + * // targetIndexId: 'f2c9a846-799f-4b19-81a4-f3096b3d6114', + * // status: 'Completed', + * // createdAt: 2025-05-07T21:42:38.971Z, + * // completedAt: 2025-05-07T21:43:11.782Z, + * // percentComplete: 100 + * // }, + * // { + * // restoreJobId: '792837b7-8001-47bf-9c11-1859826b9c10', + * // backupId: '11450b9f-96e5-47e5-9186-03f346b1f385', + * // targetIndexName: 'my-index-restore-3', + * // targetIndexId: '620dda62-c999-4dd1-b083-6beb087b31e7', + * // status: 'Pending', + * // createdAt: 2025-05-07T21:48:39.580Z, + * // completedAt: 2025-05-07T21:49:12.084Z, + * // percentComplete: 45 + * // } + * // ], + * // pagination: undefined + * // } + * ``` + * + * @param options - A {@link ListBackupsOptions} object containing the optional indexName, limit, and paginationToken values. + * @throws {@link Errors.PineconeArgumentError} when arguments passed to the method fail a runtime validation. + * @throws {@link Errors.PineconeConnectionError} when network problems or an outage of Pinecone's APIs prevent the request from being completed. + * @returns A Promise that resolves to a {@link BackupList} object. + */ + listRestoreJobs(options: ListRestoreJobsOptions) { + return this._listRestoreJobs(options); + } + /** * Targets a specific index for performing data operations. * diff --git a/utils/generateAndSeedIndex.ts b/utils/generateAndSeedIndex.ts new file mode 100644 index 00000000..4d1d6239 --- /dev/null +++ b/utils/generateAndSeedIndex.ts @@ -0,0 +1,50 @@ +import dotenv from 'dotenv'; +import { Pinecone } from '../dist'; +import { generateRecords } from '../src/integration/test-helpers'; + +const INDEX_NAME = 'local-utility-index'; +let API_KEY = ''; + +dotenv.config(); + +for (const envVar of ['PINECONE_API_KEY']) { + if (!process.env[envVar]) { + console.warn(`WARNING Missing environment variable ${envVar} in .env file`); + } else { + console.log(`INFO Found environment variable ${envVar} in .env file`); + API_KEY = process.env[envVar]; + } +} + +(async () => { + const pinecone = new Pinecone({ + apiKey: API_KEY, + }); + + console.time('create-index-duration'); + await pinecone.createIndex({ + name: INDEX_NAME, + dimension: 5, + metric: 'cosine', + spec: { serverless: { cloud: 'aws', region: 'us-east-1' } }, + suppressConflicts: true, + waitUntilReady: true, + }); + console.log( + `Index ${INDEX_NAME} created in ${console.timeEnd( + 'create-index-duration' + )}ms` + ); + + const index = pinecone.index(INDEX_NAME); + const recordsToUpsert = generateRecords({ dimension: 5, quantity: 100 }); + + console.time('seed-index-duration'); + await index.namespace('first-namespace').upsert(recordsToUpsert); + await index.namespace('second-namespace').upsert(recordsToUpsert); + console.log( + `Index namespaces seeded in ${console.timeEnd('seed-index-duration')}ms` + ); + + process.exit(); +})();