From 808366c5e4b20d37c71c94f1cb3368879c86aaec Mon Sep 17 00:00:00 2001 From: Jen Hamon Date: Fri, 6 Oct 2023 17:39:23 -0400 Subject: [PATCH 1/7] Overhaul error handling --- package.json | 4 +- src/control/__tests__/configureIndex.test.ts | 93 ------------ .../__tests__/createCollection.test.ts | 71 +--------- src/control/__tests__/createIndex.test.ts | 56 +------- .../__tests__/deleteCollection.test.ts | 73 +--------- src/control/__tests__/deleteIndex.test.ts | 63 +-------- .../__tests__/describeCollection.test.ts | 75 +--------- src/control/__tests__/describeIndex.test.ts | 63 +-------- src/control/__tests__/listCollections.test.ts | 40 ------ src/control/__tests__/listIndexes.test.ts | 34 ----- src/control/configureIndex.ts | 10 +- src/control/createCollection.ts | 10 +- src/control/createIndex.ts | 14 +- src/control/deleteCollection.ts | 10 +- src/control/deleteIndex.ts | 10 +- src/control/describeCollection.ts | 26 ++-- src/control/describeIndex.ts | 12 +- src/control/listCollections.ts | 20 +-- src/control/listIndexes.ts | 20 +-- src/control/utils.ts | 60 -------- src/data/__tests__/deleteAll.test.ts | 38 +---- src/data/__tests__/deleteMany.test.ts | 38 +---- src/data/__tests__/deleteOne.test.ts | 38 +---- src/data/__tests__/describeIndexStats.test.ts | 40 +----- src/data/__tests__/fetch.test.ts | 38 +---- src/data/__tests__/update.test.ts | 38 +---- src/data/__tests__/upsert.test.ts | 38 +---- src/data/deleteAll.ts | 12 +- src/data/deleteMany.ts | 10 +- src/data/deleteOne.ts | 12 +- src/data/describeIndexStats.ts | 40 +++--- src/data/fetch.ts | 26 ++-- src/data/projectIdSingleton.ts | 11 +- src/data/query.ts | 26 ++-- src/data/update.ts | 16 +-- src/data/upsert.ts | 22 ++- src/data/vectorOperationsProvider.ts | 2 + src/errors/base.ts | 7 +- src/errors/config.ts | 15 -- src/errors/handling.ts | 54 +++---- src/errors/http.ts | 4 +- src/errors/index.ts | 4 +- src/errors/request.ts | 48 ++++++- .../control/configureIndex.test.ts | 133 ++++++++++++++++++ src/integration/control/createIndex.test.ts | 124 ++++++++++++++++ src/integration/control/describeIndex.test.ts | 45 ++++++ src/integration/control/listIndexes.test.ts | 29 ++++ src/integration/data/deleteMany.test.ts | 2 +- src/integration/data/query.test.ts | 2 +- src/integration/errorHandling.test.ts | 91 ++++++++++++ src/integration/test-helpers.ts | 22 +++ src/pinecone.ts | 12 +- src/utils/middleware.ts | 24 ++++ src/utils/testHelper.ts | 11 ++ 54 files changed, 680 insertions(+), 1156 deletions(-) delete mode 100644 src/control/utils.ts create mode 100644 src/integration/control/configureIndex.test.ts create mode 100644 src/integration/control/createIndex.test.ts create mode 100644 src/integration/control/describeIndex.test.ts create mode 100644 src/integration/control/listIndexes.test.ts create mode 100644 src/integration/errorHandling.test.ts create mode 100644 src/utils/middleware.ts create mode 100644 src/utils/testHelper.ts diff --git a/package.json b/package.json index 97f52d76..d4488a81 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,8 @@ "format": "prettier --write .", "lint": "eslint src/ --ext .ts", "repl": "npm run build && node utils/replInit.ts", - "test:integration:node": "jest src/integration/ -c jest.config.integration-node.js", - "test:integration:edge": "jest src/integration/ -c jest.config.integration-edge.js", + "test:integration:node": "TEST_ENV=node jest src/integration/ -c jest.config.integration-node.js --runInBand --bail", + "test:integration:edge": "TEST_ENV=edge jest src/integration/ -c jest.config.integration-edge.js --runInBand --bail", "test:unit": "jest src/" }, "engines": { diff --git a/src/control/__tests__/configureIndex.test.ts b/src/control/__tests__/configureIndex.test.ts index b76e70b4..148ff3c6 100644 --- a/src/control/__tests__/configureIndex.test.ts +++ b/src/control/__tests__/configureIndex.test.ts @@ -1,9 +1,4 @@ import { configureIndex } from '../configureIndex'; -import { - PineconeBadRequestError, - PineconeInternalServerError, - PineconeNotFoundError, -} from '../../errors'; import { IndexOperationsApi } from '../../pinecone-generated-ts-fetch'; import type { ConfigureIndexRequest } from '../../pinecone-generated-ts-fetch'; @@ -13,9 +8,6 @@ describe('configureIndex', () => { jest.fn(); const IOA = { configureIndex: fakeConfigure } as IndexOperationsApi; - jest.mock('../../pinecone-generated-ts-fetch', () => ({ - IndexOperationsApi: IOA, - })); const returned = await configureIndex(IOA)('index-name', { replicas: 10 }); expect(returned).toBe(void 0); @@ -24,89 +16,4 @@ describe('configureIndex', () => { patchRequest: { replicas: 10 }, }); }); - - describe('http error mapping', () => { - test('when 500 occurs', async () => { - const fakeConfigure: (req: ConfigureIndexRequest) => Promise = - jest.fn().mockImplementation(() => - Promise.reject({ - response: { - status: 500, - text: () => 'backend error message', - }, - }) - ); - const IOA = { - configureIndex: fakeConfigure, - } as IndexOperationsApi; - - jest.mock('../../pinecone-generated-ts-fetch', () => ({ - IndexOperationsApi: IOA, - })); - - const toThrow = async () => { - await configureIndex(IOA)('index-name', { replicas: 10 }); - }; - - await expect(toThrow).rejects.toThrow(PineconeInternalServerError); - }); - - test('when 400 occurs, displays server message', async () => { - const fakeConfigure: (req: ConfigureIndexRequest) => Promise = - jest.fn().mockImplementation(() => - Promise.reject({ - response: { - status: 400, - text: () => 'backend error message', - }, - }) - ); - const IOA = { - configureIndex: fakeConfigure, - } as IndexOperationsApi; - - jest.mock('../../pinecone-generated-ts-fetch', () => ({ - IndexOperationsApi: IOA, - })); - - const toThrow = async () => { - await configureIndex(IOA)('index-name', { replicas: 10 }); - }; - - await expect(toThrow).rejects.toThrow(PineconeBadRequestError); - await expect(toThrow).rejects.toThrow('backend error message'); - }); - - test('when 404 occurs, show available indexes', async () => { - const fakeConfigure: (req: ConfigureIndexRequest) => Promise = - jest.fn().mockImplementation(() => - Promise.reject({ - response: { - status: 404, - text: () => 'not found', - }, - }) - ); - const fakeListIndexes: () => Promise = jest - .fn() - .mockImplementation(() => Promise.resolve(['foo', 'bar'])); - const IOA = { - configureIndex: fakeConfigure, - listIndexes: fakeListIndexes, - } as IndexOperationsApi; - - jest.mock('../../pinecone-generated-ts-fetch', () => ({ - IndexOperationsApi: IOA, - })); - - const toThrow = async () => { - await configureIndex(IOA)('index-name', { replicas: 10 }); - }; - - await expect(toThrow).rejects.toThrow(PineconeNotFoundError); - await expect(toThrow).rejects.toThrow( - "Index 'index-name' does not exist. Valid index names: ['foo', 'bar']" - ); - }); - }); }); diff --git a/src/control/__tests__/createCollection.test.ts b/src/control/__tests__/createCollection.test.ts index c676734b..f0b58528 100644 --- a/src/control/__tests__/createCollection.test.ts +++ b/src/control/__tests__/createCollection.test.ts @@ -1,10 +1,5 @@ import { createCollection } from '../createCollection'; -import { - PineconeArgumentError, - PineconeBadRequestError, - PineconeInternalServerError, - PineconeNotFoundError, -} from '../../errors'; +import { PineconeArgumentError } from '../../errors'; import { IndexOperationsApi } from '../../pinecone-generated-ts-fetch'; import type { CreateCollectionOperationRequest as CCOR } from '../../pinecone-generated-ts-fetch'; @@ -149,68 +144,4 @@ describe('createCollection', () => { }, }); }); - - describe('http error mapping', () => { - test('when 500 occurs', async () => { - const IOA = setOpenAPIResponse(() => - Promise.reject({ - response: { - status: 500, - text: () => 'backend error message', - }, - }) - ); - - const toThrow = async () => { - await createCollection(IOA)({ - name: 'collection-name', - source: 'index-name', - }); - }; - - await expect(toThrow).rejects.toThrow(PineconeInternalServerError); - }); - - test('when 400 occurs, displays server message', async () => { - const IOA = setOpenAPIResponse(() => - Promise.reject({ - response: { - status: 400, - text: () => 'backend error message', - }, - }) - ); - const toThrow = async () => { - await createCollection(IOA)({ - name: 'collection-name', - source: 'index-name', - }); - }; - - await expect(toThrow).rejects.toThrow(PineconeBadRequestError); - await expect(toThrow).rejects.toThrow('backend error message'); - }); - - test('when 404 occurs, show available indexes', async () => { - const IOA = setOpenAPIResponse(() => - Promise.reject({ - response: { - status: 404, - text: () => 'not found', - }, - }) - ); - const toThrow = async () => { - await createCollection(IOA)({ - name: 'collection-name', - source: 'index-name', - }); - }; - - await expect(toThrow).rejects.toThrow(PineconeNotFoundError); - await expect(toThrow).rejects.toThrow( - "Index 'index-name' does not exist. Valid index names: ['foo', 'bar']" - ); - }); - }); }); diff --git a/src/control/__tests__/createIndex.test.ts b/src/control/__tests__/createIndex.test.ts index aefd543c..e30fbe06 100644 --- a/src/control/__tests__/createIndex.test.ts +++ b/src/control/__tests__/createIndex.test.ts @@ -1,9 +1,4 @@ import { createIndex } from '../createIndex'; -import { - PineconeBadRequestError, - PineconeConflictError, - PineconeInternalServerError, -} from '../../errors'; import { IndexOperationsApi } from '../../pinecone-generated-ts-fetch'; import type { CreateIndexRequest, @@ -23,7 +18,7 @@ const setupCreateIndexResponse = ( .mockImplementation(() => isCreateIndexSuccess ? Promise.resolve(createIndexResponse) - : Promise.reject({ response: createIndexResponse }) + : Promise.reject(createIndexResponse) ); // unfold describeIndexResponse @@ -134,53 +129,4 @@ describe('createIndex', () => { }); }); }); - - describe('http error mapping', () => { - test('when 500 occurs', async () => { - const IOA = setupCreateIndexResponse( - { status: 500, text: () => 'backend error message' }, - undefined, - false - ); - - const toThrow = async () => { - const createIndexFn = createIndex(IOA); - await createIndexFn({ name: 'index-name', dimension: 10 }); - }; - - await expect(toThrow).rejects.toThrow(PineconeInternalServerError); - }); - - test('when 400 occurs, displays server message', async () => { - const serverError = 'there has been a server error!'; - const IOA = setupCreateIndexResponse( - { status: 400, text: () => serverError }, - undefined, - false - ); - - const toThrow = async () => { - const createIndexFn = createIndex(IOA); - await createIndexFn({ name: 'index-name', dimension: 10 }); - }; - - await expect(toThrow).rejects.toThrow(PineconeBadRequestError); - await expect(toThrow).rejects.toThrow(serverError); - }); - - test('when 409 occurs', async () => { - const IOA = setupCreateIndexResponse( - { status: 409, text: () => 'conflict error message' }, - undefined, - false - ); - - const toThrow = async () => { - const createIndexFn = createIndex(IOA); - await createIndexFn({ name: 'index-name', dimension: 10 }); - }; - - await expect(toThrow).rejects.toThrow(PineconeConflictError); - }); - }); }); diff --git a/src/control/__tests__/deleteCollection.test.ts b/src/control/__tests__/deleteCollection.test.ts index 1cc2713d..e58d8cc7 100644 --- a/src/control/__tests__/deleteCollection.test.ts +++ b/src/control/__tests__/deleteCollection.test.ts @@ -1,9 +1,5 @@ import { deleteCollection } from '../deleteCollection'; -import { - PineconeArgumentError, - PineconeInternalServerError, - PineconeNotFoundError, -} from '../../errors'; +import { PineconeArgumentError } from '../../errors'; import { IndexOperationsApi } from '../../pinecone-generated-ts-fetch'; import type { DeleteCollectionRequest as DCR } from '../../pinecone-generated-ts-fetch'; @@ -59,71 +55,4 @@ describe('deleteCollection', () => { ); }); }); - - describe('uses http error mapper', () => { - test('it should map errors with the http error mapper (500)', async () => { - const IOA = setupMocks(() => - Promise.reject({ response: { status: 500, text: async () => '' } }) - ); - - // @ts-ignore - const expectToThrow = async () => - await deleteCollection(IOA)('collection-name'); - - expect(expectToThrow).rejects.toThrowError(PineconeInternalServerError); - }); - }); - - describe('custom error mapping', () => { - test('not found (404), fetches and shows available collection names', async () => { - const IOA = setupMocks( - () => - Promise.reject({ response: { status: 404, text: async () => '' } }), - () => Promise.resolve(['foo', 'bar']) - ); - - // @ts-ignore - const expectToThrow = async () => - await deleteCollection(IOA)('collection-name'); - - expect(expectToThrow).rejects.toThrowError(PineconeNotFoundError); - expect(expectToThrow).rejects.toThrowError( - `Collection 'collection-name' does not exist. Valid collection names: ['foo', 'bar']` - ); - }); - - test('not found (404), fetches and shows available collection names (empty list)', async () => { - const IOA = setupMocks( - () => - Promise.reject({ response: { status: 404, text: async () => '' } }), - () => Promise.resolve([]) - ); - - // @ts-ignore - const expectToThrow = async () => - await deleteCollection(IOA)('collection-name'); - - expect(expectToThrow).rejects.toThrowError(PineconeNotFoundError); - expect(expectToThrow).rejects.toThrowError( - `Collection 'collection-name' does not exist. Valid collection names: []` - ); - }); - - test('not found (404), error while fetching collection list', async () => { - const IOA = setupMocks( - () => - Promise.reject({ response: { status: 404, text: async () => '' } }), - () => Promise.reject('error') - ); - - // @ts-ignore - const expectToThrow = async () => - await deleteCollection(IOA)('collection-name'); - - expect(expectToThrow).rejects.toThrowError(PineconeNotFoundError); - expect(expectToThrow).rejects.toThrowError( - `Collection 'collection-name' does not exist.` - ); - }); - }); }); diff --git a/src/control/__tests__/deleteIndex.test.ts b/src/control/__tests__/deleteIndex.test.ts index 0e0d5c0e..be59b484 100644 --- a/src/control/__tests__/deleteIndex.test.ts +++ b/src/control/__tests__/deleteIndex.test.ts @@ -1,9 +1,5 @@ import { deleteIndex } from '../deleteIndex'; -import { - PineconeArgumentError, - PineconeInternalServerError, - PineconeNotFoundError, -} from '../../errors'; +import { PineconeArgumentError } from '../../errors'; describe('deleteIndex', () => { const setupSuccessResponse = (responseData) => { @@ -14,17 +10,6 @@ describe('deleteIndex', () => { }; }; - const setupErrorResponse = (response) => { - return { - deleteIndex: jest - .fn() - .mockImplementation(() => Promise.reject({ response })), - listIndexes: jest - .fn() - .mockImplementation(() => Promise.resolve(['foo', 'bar'])), - }; - }; - describe('argument validation', () => { test('should throw if index name is not provided', async () => { const IOA = setupSuccessResponse(''); @@ -62,50 +47,4 @@ describe('deleteIndex', () => { ); }); }); - - describe('uses http error mapper', () => { - test('it should map errors with the http error mapper (500)', async () => { - const IOA = setupErrorResponse({ status: 500, text: async () => '' }); - - // @ts-ignore - const expectToThrow = async () => await deleteIndex(IOA)('index-name'); - - expect(expectToThrow).rejects.toThrowError(PineconeInternalServerError); - }); - }); - - describe('custom error mapping', () => { - test('not found (404), fetches and shows available index names', async () => { - const IOA = setupErrorResponse({ status: 404, text: async () => '' }); - - // @ts-ignore - const expectToThrow = async () => await deleteIndex(IOA)('index-name'); - - expect(expectToThrow).rejects.toThrowError(PineconeNotFoundError); - expect(expectToThrow).rejects.toThrowError( - `Index 'index-name' does not exist. Valid index names: ['foo', 'bar']` - ); - }); - - test('not found (404), error while fetching index list', async () => { - const IOA = { - deleteIndex: jest - .fn() - .mockImplementation(() => - Promise.reject({ response: { status: 404, text: async () => '' } }) - ), - listIndexes: jest - .fn() - .mockImplementation(() => Promise.reject('error')), - }; - - // @ts-ignore - const expectToThrow = async () => await deleteIndex(IOA)('index-name'); - - expect(expectToThrow).rejects.toThrowError(PineconeNotFoundError); - expect(expectToThrow).rejects.toThrowError( - `Index 'index-name' does not exist.` - ); - }); - }); }); diff --git a/src/control/__tests__/describeCollection.test.ts b/src/control/__tests__/describeCollection.test.ts index 9faa5a4d..4459cb56 100644 --- a/src/control/__tests__/describeCollection.test.ts +++ b/src/control/__tests__/describeCollection.test.ts @@ -1,9 +1,5 @@ import { describeCollection } from '../describeCollection'; -import { - PineconeArgumentError, - PineconeInternalServerError, - PineconeNotFoundError, -} from '../../errors'; +import { PineconeArgumentError } from '../../errors'; import { IndexOperationsApi } from '../../pinecone-generated-ts-fetch'; import type { DescribeCollectionRequest as DCR, @@ -96,73 +92,4 @@ describe('describeCollection', () => { }); }); }); - - describe('uses http error mapper', () => { - test('it should map errors with the http error mapper (500)', async () => { - const IOA = setupMocks( - () => - Promise.reject({ response: { status: 500, text: async () => '' } }), - () => Promise.resolve([]) - ); - - // @ts-ignore - const expectToThrow = async () => - await describeCollection(IOA)('collection-name'); - - expect(expectToThrow).rejects.toThrowError(PineconeInternalServerError); - }); - }); - - describe('custom error mapping', () => { - test('not found (404), fetches and shows available collection names', async () => { - const IOA = setupMocks( - () => - Promise.reject({ response: { status: 404, text: async () => '' } }), - () => Promise.resolve(['foo', 'bar']) - ); - - // @ts-ignore - const expectToThrow = async () => - await describeCollection(IOA)('collection-name'); - - expect(expectToThrow).rejects.toThrowError(PineconeNotFoundError); - expect(expectToThrow).rejects.toThrowError( - `Collection 'collection-name' does not exist. Valid collection names: ['foo', 'bar']` - ); - }); - - test('not found (404), fetches and shows available collection names (empty list)', async () => { - const IOA = setupMocks( - () => - Promise.reject({ response: { status: 404, text: async () => '' } }), - () => Promise.resolve([]) - ); - - // @ts-ignore - const expectToThrow = async () => - await describeCollection(IOA)('collection-name'); - - expect(expectToThrow).rejects.toThrowError(PineconeNotFoundError); - expect(expectToThrow).rejects.toThrowError( - `Collection 'collection-name' does not exist. Valid collection names: []` - ); - }); - - test('not found (404), error while fetching collection list', async () => { - const IOA = setupMocks( - () => - Promise.reject({ response: { status: 404, text: async () => '' } }), - () => Promise.reject('error') - ); - - // @ts-ignore - const expectToThrow = async () => - await describeCollection(IOA)('collection-name'); - - expect(expectToThrow).rejects.toThrowError(PineconeNotFoundError); - expect(expectToThrow).rejects.toThrowError( - `Collection 'collection-name' does not exist.` - ); - }); - }); }); diff --git a/src/control/__tests__/describeIndex.test.ts b/src/control/__tests__/describeIndex.test.ts index 08a2041f..672ae75d 100644 --- a/src/control/__tests__/describeIndex.test.ts +++ b/src/control/__tests__/describeIndex.test.ts @@ -1,9 +1,5 @@ import { describeIndex } from '../describeIndex'; -import { - PineconeArgumentError, - PineconeInternalServerError, - PineconeNotFoundError, -} from '../../errors'; +import { PineconeArgumentError } from '../../errors'; describe('describeIndex', () => { let responseData; @@ -16,17 +12,6 @@ describe('describeIndex', () => { }; }; - const setupErrorResponse = (response) => { - return { - describeIndex: jest - .fn() - .mockImplementation(() => Promise.reject({ response })), - listIndexes: jest - .fn() - .mockImplementation(() => Promise.resolve(['foo', 'bar'])), - }; - }; - beforeEach(() => { responseData = Object.freeze({ database: { @@ -101,50 +86,4 @@ describe('describeIndex', () => { ); }); }); - - describe('uses http error mapper', () => { - test('it should map errors with the http error mapper (500)', async () => { - const IOA = setupErrorResponse({ status: 500, text: async () => '' }); - - // @ts-ignore - const expectToThrow = async () => await describeIndex(IOA)('index-name'); - - expect(expectToThrow).rejects.toThrowError(PineconeInternalServerError); - }); - }); - - describe('custom error mapping', () => { - test('not found (404), fetches and shows available index names', async () => { - const IOA = setupErrorResponse({ status: 404, text: async () => '' }); - - // @ts-ignore - const expectToThrow = async () => await describeIndex(IOA)('index-name'); - - expect(expectToThrow).rejects.toThrowError(PineconeNotFoundError); - expect(expectToThrow).rejects.toThrowError( - `Index 'index-name' does not exist. Valid index names: ['foo', 'bar']` - ); - }); - - test('not found (404), error while fetching index list', async () => { - const IOA = { - describeIndex: jest - .fn() - .mockImplementation(() => - Promise.reject({ response: { status: 404, text: async () => '' } }) - ), - listIndexes: jest - .fn() - .mockImplementation(() => Promise.reject('error')), - }; - - // @ts-ignore - const expectToThrow = async () => await describeIndex(IOA)('index-name'); - - expect(expectToThrow).rejects.toThrowError(PineconeNotFoundError); - expect(expectToThrow).rejects.toThrowError( - `Index 'index-name' does not exist.` - ); - }); - }); }); diff --git a/src/control/__tests__/listCollections.test.ts b/src/control/__tests__/listCollections.test.ts index 5f5349dc..173ff724 100644 --- a/src/control/__tests__/listCollections.test.ts +++ b/src/control/__tests__/listCollections.test.ts @@ -1,8 +1,4 @@ import { listCollections } from '../listCollections'; -import { - PineconeInternalServerError, - PineconeAuthorizationError, -} from '../../errors'; describe('listCollections', () => { test('should return a list of collection objects', async () => { @@ -22,40 +18,4 @@ describe('listCollections', () => { { name: 'collection-name-2' }, ]); }); - - test('it should map errors with the http error mapper (500)', async () => { - const IOA = { - listCollections: jest.fn().mockImplementation(() => - Promise.reject({ - response: { - status: 500, - text: async () => 'Internal Server Error', - }, - }) - ), - }; - - // @ts-ignore - const expectToThrow = async () => await listCollections(IOA)(); - - expect(expectToThrow).rejects.toThrowError(PineconeInternalServerError); - }); - - test('it should map errors with the http error mapper (401)', async () => { - const IOA = { - listCollections: jest.fn().mockImplementation(() => - Promise.reject({ - response: { - status: 401, - text: async () => 'Unauthorized', - }, - }) - ), - }; - - // @ts-ignore - const expectToThrow = async () => await listCollections(IOA)(); - - expect(expectToThrow).rejects.toThrowError(PineconeAuthorizationError); - }); }); diff --git a/src/control/__tests__/listIndexes.test.ts b/src/control/__tests__/listIndexes.test.ts index 93aa6549..5fc05ffb 100644 --- a/src/control/__tests__/listIndexes.test.ts +++ b/src/control/__tests__/listIndexes.test.ts @@ -1,8 +1,4 @@ import { listIndexes } from '../listIndexes'; -import { - PineconeInternalServerError, - PineconeAuthorizationError, -} from '../../errors'; describe('listIndexes', () => { test('should return a list of index objects', async () => { @@ -22,34 +18,4 @@ describe('listIndexes', () => { { name: 'index-name-2' }, ]); }); - - test('it should map errors with the http error mapper (500)', async () => { - const IndexOperationsApi = { - listIndexes: jest - .fn() - .mockImplementation(() => - Promise.reject({ response: { status: 500, text: async () => '' } }) - ), - }; - - // @ts-ignore - const expectToThrow = async () => await listIndexes(IndexOperationsApi)(); - - expect(expectToThrow).rejects.toThrowError(PineconeInternalServerError); - }); - - test('it should map errors with the http error mapper (401)', async () => { - const IndexOperationsApi = { - listIndexes: jest - .fn() - .mockImplementation(() => - Promise.reject({ response: { status: 401, text: async () => '' } }) - ), - }; - - // @ts-ignore - const expectToThrow = async () => await listIndexes(IndexOperationsApi)(); - - expect(expectToThrow).rejects.toThrowError(PineconeAuthorizationError); - }); }); diff --git a/src/control/configureIndex.ts b/src/control/configureIndex.ts index a525d645..c7557d2e 100644 --- a/src/control/configureIndex.ts +++ b/src/control/configureIndex.ts @@ -2,7 +2,6 @@ import { IndexOperationsApi } from '../pinecone-generated-ts-fetch'; import { PineconeArgumentError } from '../errors'; import { buildValidator } from '../validator'; import type { IndexName, PodType } from './types'; -import { handleIndexRequestError } from './utils'; import { Type } from '@sinclair/typebox'; import { ReplicasSchema, PodTypeSchema, IndexNameSchema } from './types'; @@ -49,12 +48,7 @@ export const configureIndex = (api: IndexOperationsApi) => { ); } - try { - await api.configureIndex({ indexName: name, patchRequest: options }); - return; - } catch (e) { - const err = await handleIndexRequestError(e, api, name); - throw err; - } + await api.configureIndex({ indexName: name, patchRequest: options }); + return; }; }; diff --git a/src/control/createCollection.ts b/src/control/createCollection.ts index 8265189a..4d999ab4 100644 --- a/src/control/createCollection.ts +++ b/src/control/createCollection.ts @@ -1,6 +1,5 @@ import { IndexOperationsApi } from '../pinecone-generated-ts-fetch'; import { buildConfigValidator } from '../validator'; -import { handleIndexRequestError } from './utils'; import { CollectionNameSchema, IndexNameSchema } from './types'; import type { CollectionName, IndexName } from './types'; import { Type } from '@sinclair/typebox'; @@ -33,12 +32,7 @@ export const createCollection = (api: IndexOperationsApi) => { return async (options: CreateCollectionOptions): Promise => { validator(options); - try { - await api.createCollection({ createCollectionRequest: options }); - return; - } catch (e) { - const err = await handleIndexRequestError(e, api, options.source); - throw err; - } + await api.createCollection({ createCollectionRequest: options }); + return; }; }; diff --git a/src/control/createIndex.ts b/src/control/createIndex.ts index 1ea90470..326bc83b 100644 --- a/src/control/createIndex.ts +++ b/src/control/createIndex.ts @@ -2,7 +2,6 @@ import { IndexOperationsApi } from '../pinecone-generated-ts-fetch'; import { buildConfigValidator } from '../validator'; import { debugLog } from '../utils'; import { handleApiError } from '../errors'; -import { handleIndexRequestError } from './utils'; import { Type } from '@sinclair/typebox'; import { IndexNameSchema, @@ -97,13 +96,16 @@ export const createIndex = (api: IndexOperationsApi) => { if (options.waitUntilReady) { return await waitUntilIndexIsReady(api, options.name); } - return; } catch (e) { - const err = await handleIndexRequestError(e, api, options.name); - if (options.suppressConflicts && err.name === 'PineconeConflictError') { - return; + if ( + !( + options.suppressConflicts && + e instanceof Error && + e.name === 'PineconeConflictError' + ) + ) { + throw e; } - throw err; } }; }; diff --git a/src/control/deleteCollection.ts b/src/control/deleteCollection.ts index b4728689..80453334 100644 --- a/src/control/deleteCollection.ts +++ b/src/control/deleteCollection.ts @@ -1,6 +1,5 @@ import { IndexOperationsApi } from '../pinecone-generated-ts-fetch'; import { buildConfigValidator } from '../validator'; -import { handleCollectionRequestError } from './utils'; import { CollectionNameSchema } from './types'; import type { CollectionName } from './types'; @@ -18,12 +17,7 @@ export const deleteCollection = (api: IndexOperationsApi) => { return async (collectionName: CollectionName): Promise => { validator(collectionName); - try { - await api.deleteCollection({ collectionName: collectionName }); - return; - } catch (e) { - const err = await handleCollectionRequestError(e, api, collectionName); - throw err; - } + await api.deleteCollection({ collectionName: collectionName }); + return; }; }; diff --git a/src/control/deleteIndex.ts b/src/control/deleteIndex.ts index 759ff31f..0d9b1d10 100644 --- a/src/control/deleteIndex.ts +++ b/src/control/deleteIndex.ts @@ -1,7 +1,6 @@ import { IndexOperationsApi } from '../pinecone-generated-ts-fetch'; import { buildConfigValidator } from '../validator'; import { IndexName, IndexNameSchema } from './types'; -import { handleIndexRequestError } from './utils'; /** The name of index to delete */ export type DeleteIndexOptions = IndexName; @@ -12,12 +11,7 @@ export const deleteIndex = (api: IndexOperationsApi) => { return async (indexName: DeleteIndexOptions): Promise => { validator(indexName); - try { - await api.deleteIndex({ indexName: indexName }); - return; - } catch (e) { - const err = await handleIndexRequestError(e, api, indexName); - throw err; - } + await api.deleteIndex({ indexName: indexName }); + return; }; }; diff --git a/src/control/describeCollection.ts b/src/control/describeCollection.ts index fe240229..37765933 100644 --- a/src/control/describeCollection.ts +++ b/src/control/describeCollection.ts @@ -1,6 +1,5 @@ import { IndexOperationsApi } from '../pinecone-generated-ts-fetch'; import { buildConfigValidator } from '../validator'; -import { handleCollectionRequestError } from './utils'; import { CollectionNameSchema } from './types'; import type { CollectionName } from './types'; @@ -40,20 +39,15 @@ export const describeCollection = (api: IndexOperationsApi) => { return async (name: CollectionName): Promise => { validator(name); - try { - const result = await api.describeCollection({ collectionName: name }); - - // Alias vectorCount to recordCount - return { - name: result.name, - size: result.size, - status: result.status, - dimension: result.dimension, - recordCount: result.vectorCount, - }; - } catch (e) { - const err = await handleCollectionRequestError(e, api, name); - throw err; - } + const result = await api.describeCollection({ collectionName: name }); + + // Alias vectorCount to recordCount + return { + name: result.name, + size: result.size, + status: result.status, + dimension: result.dimension, + recordCount: result.vectorCount, + }; }; }; diff --git a/src/control/describeIndex.ts b/src/control/describeIndex.ts index 75ad70c8..e3ad05b9 100644 --- a/src/control/describeIndex.ts +++ b/src/control/describeIndex.ts @@ -1,7 +1,6 @@ import { IndexOperationsApi } from '../pinecone-generated-ts-fetch'; import { buildConfigValidator } from '../validator'; import type { IndexMeta } from '../pinecone-generated-ts-fetch'; -import { handleIndexRequestError } from './utils'; import { IndexNameSchema } from './types'; import type { IndexName } from './types'; @@ -26,13 +25,8 @@ export const describeIndex = (api: IndexOperationsApi) => { return async (name: IndexName): Promise => { validator(name); - try { - const result = await api.describeIndex({ indexName: name }); - removeDeprecatedFields(result); - return result; - } catch (e) { - const err = await handleIndexRequestError(e, api, name); - throw err; - } + const result = await api.describeIndex({ indexName: name }); + removeDeprecatedFields(result); + return result; }; }; diff --git a/src/control/listCollections.ts b/src/control/listCollections.ts index 2a44fd00..33e3db89 100644 --- a/src/control/listCollections.ts +++ b/src/control/listCollections.ts @@ -1,5 +1,4 @@ import { IndexOperationsApi } from '../pinecone-generated-ts-fetch'; -import { handleApiError } from '../errors'; /** * A partial description of a collection in your project. @@ -20,18 +19,13 @@ export type CollectionList = PartialCollectionDescription[]; export const listCollections = (api: IndexOperationsApi) => { return async (): Promise => { - try { - const results = await api.listCollections(); + const results = await api.listCollections(); - // We know in a future version of the API that listing - // collections should return more information than just the - // collection names. Mapping these results into an object - // will allow us us to add more information in the future - // in a non-breaking way. - return results.map((c) => ({ name: c })); - } catch (e) { - const err = await handleApiError(e); - throw err; - } + // We know in a future version of the API that listing + // collections should return more information than just the + // collection names. Mapping these results into an object + // will allow us us to add more information in the future + // in a non-breaking way. + return results.map((c) => ({ name: c })); }; }; diff --git a/src/control/listIndexes.ts b/src/control/listIndexes.ts index 59b56d4b..63d42935 100644 --- a/src/control/listIndexes.ts +++ b/src/control/listIndexes.ts @@ -1,5 +1,4 @@ import { IndexOperationsApi } from '../pinecone-generated-ts-fetch'; -import { handleApiError } from '../errors'; /** * A partial description of indexes in your project. @@ -17,18 +16,13 @@ export type IndexList = Array; export const listIndexes = (api: IndexOperationsApi) => { return async (): Promise => { - try { - const names = await api.listIndexes(); + const names = await api.listIndexes(); - // We know in a future version of the API that listing - // indexes should return more information than just the - // index names. Mapping these results into an object - // will allow us us to add more information in the future - // in a non-breaking way. - return names.map((n) => ({ name: n })); - } catch (e) { - const err = await handleApiError(e); - throw err; - } + // We know in a future version of the API that listing + // indexes should return more information than just the + // index names. Mapping these results into an object + // will allow us us to add more information in the future + // in a non-breaking way. + return names.map((n) => ({ name: n })); }; }; diff --git a/src/control/utils.ts b/src/control/utils.ts deleted file mode 100644 index 4ddbef73..00000000 --- a/src/control/utils.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { IndexOperationsApi } from '../pinecone-generated-ts-fetch'; -import { handleApiError } from '../errors'; - -export const validIndexMessage = async ( - api: IndexOperationsApi, - name: string -) => { - try { - const validNames = await api.listIndexes(); - return `Index '${name}' does not exist. Valid index names: [${validNames - .map((n) => `'${n}'`) - .join(', ')}]`; - } catch (e) { - // Expect to end up here only if a second error occurs while fetching valid index names. - // In that case, we can just return a message without the additional context about what names - // are valid. - return `Index '${name}' does not exist.`; - } -}; - -export const validCollectionMessage = async ( - api: IndexOperationsApi, - name: string -) => { - try { - const validNames = await api.listCollections(); - return `Collection '${name}' does not exist. Valid collection names: [${validNames - .map((n) => `'${n}'`) - .join(', ')}]`; - } catch (e) { - // Expect to end up here only if a second error occurs while fetching valid collection names. - // We can show the error from the failed call to describeIndex, but without listing - // index names. - return `Collection '${name}' does not exist.`; - } -}; - -export const handleIndexRequestError = async ( - e: any, - api: IndexOperationsApi, - indexName: string -): Promise => { - return await handleApiError(e, async (statusCode, rawMessageText) => - statusCode === 404 - ? await validIndexMessage(api, indexName) - : rawMessageText - ); -}; - -export const handleCollectionRequestError = async ( - e: any, - api: IndexOperationsApi, - collectionName: string -): Promise => { - return await handleApiError(e, async (statusCode, rawMessageText) => - statusCode === 404 - ? await validCollectionMessage(api, collectionName) - : rawMessageText - ); -}; diff --git a/src/data/__tests__/deleteAll.test.ts b/src/data/__tests__/deleteAll.test.ts index 5f0b663c..431e0d0b 100644 --- a/src/data/__tests__/deleteAll.test.ts +++ b/src/data/__tests__/deleteAll.test.ts @@ -1,9 +1,5 @@ import { deleteAll } from '../deleteAll'; -import { - PineconeBadRequestError, - PineconeInternalServerError, -} from '../../errors'; -import { setupDeleteFailure, setupDeleteSuccess } from './deleteOne.test'; +import { setupDeleteSuccess } from './deleteOne.test'; describe('deleteAll', () => { test('calls the openapi delete endpoint, passing deleteAll with target namespace', async () => { @@ -17,36 +13,4 @@ describe('deleteAll', () => { deleteRequest: { deleteAll: true, namespace: 'namespace' }, }); }); - - describe('http error mapping', () => { - test('when 500 occurs', async () => { - const { VoaProvider } = setupDeleteFailure({ - status: 500, - text: () => 'backend error message', - }); - - const toThrow = async () => { - const deleteAllFn = deleteAll(VoaProvider, 'namespace'); - await deleteAllFn(); - }; - - await expect(toThrow).rejects.toThrow(PineconeInternalServerError); - }); - - test('when 400 occurs, displays server message', async () => { - const serverError = 'there has been a server error!'; - const { VoaProvider } = setupDeleteFailure({ - status: 400, - text: () => serverError, - }); - - const toThrow = async () => { - const deleteAllFn = deleteAll(VoaProvider, 'namespace'); - await deleteAllFn(); - }; - - await expect(toThrow).rejects.toThrow(PineconeBadRequestError); - await expect(toThrow).rejects.toThrow(serverError); - }); - }); }); diff --git a/src/data/__tests__/deleteMany.test.ts b/src/data/__tests__/deleteMany.test.ts index 46b14d2f..85b4763d 100644 --- a/src/data/__tests__/deleteMany.test.ts +++ b/src/data/__tests__/deleteMany.test.ts @@ -1,9 +1,5 @@ import { deleteMany } from '../deleteMany'; -import { - PineconeBadRequestError, - PineconeInternalServerError, -} from '../../errors'; -import { setupDeleteSuccess, setupDeleteFailure } from './deleteOne.test'; +import { setupDeleteSuccess } from './deleteOne.test'; describe('deleteMany', () => { test('calls the openapi delete endpoint, passing ids with target namespace', async () => { @@ -29,36 +25,4 @@ describe('deleteMany', () => { deleteRequest: { filter: { genre: 'ambient' }, namespace: 'namespace' }, }); }); - - describe('http error mapping', () => { - test('when 500 occurs', async () => { - const { VoaProvider } = setupDeleteFailure({ - status: 500, - text: () => 'backend error message', - }); - - const toThrow = async () => { - const deleteManyFn = deleteMany(VoaProvider, 'namespace'); - await deleteManyFn({ ids: ['123', '456', '789'] }); - }; - - await expect(toThrow).rejects.toThrow(PineconeInternalServerError); - }); - - test('when 400 occurs, displays server message', async () => { - const serverError = 'there has been a server error!'; - const { VoaProvider } = setupDeleteFailure({ - status: 400, - text: () => serverError, - }); - - const toThrow = async () => { - const deleteManyFn = deleteMany(VoaProvider, 'namespace'); - await deleteManyFn({ ids: ['123', '456', '789'] }); - }; - - await expect(toThrow).rejects.toThrow(PineconeBadRequestError); - await expect(toThrow).rejects.toThrow(serverError); - }); - }); }); diff --git a/src/data/__tests__/deleteOne.test.ts b/src/data/__tests__/deleteOne.test.ts index 2572d0fa..436cdfdd 100644 --- a/src/data/__tests__/deleteOne.test.ts +++ b/src/data/__tests__/deleteOne.test.ts @@ -1,8 +1,4 @@ import { deleteOne } from '../deleteOne'; -import { - PineconeBadRequestError, - PineconeInternalServerError, -} from '../../errors'; import type { DeleteOperationRequest, VectorOperationsApi, @@ -13,7 +9,7 @@ const setupDeleteResponse = (response, isSuccess) => { const fakeDelete: (req: DeleteOperationRequest) => Promise = jest .fn() .mockImplementation(() => - isSuccess ? Promise.resolve(response) : Promise.reject({ response }) + isSuccess ? Promise.resolve(response) : Promise.reject(response) ); const VOA = { _delete: fakeDelete } as VectorOperationsApi; const VoaProvider = { provide: async () => VOA } as VectorOperationsProvider; @@ -38,36 +34,4 @@ describe('deleteOne', () => { deleteRequest: { ids: ['123'], namespace: 'namespace' }, }); }); - - describe('http error mapping', () => { - test('when 500 occurs', async () => { - const { VoaProvider } = setupDeleteFailure({ - status: 500, - text: () => 'backend error message', - }); - - const toThrow = async () => { - const deleteOneFn = deleteOne(VoaProvider, 'namespace'); - await deleteOneFn('123'); - }; - - await expect(toThrow).rejects.toThrow(PineconeInternalServerError); - }); - - test('when 400 occurs, displays server message', async () => { - const serverError = 'there has been a server error!'; - const { VoaProvider } = setupDeleteFailure({ - status: 400, - text: () => serverError, - }); - - const toThrow = async () => { - const deleteOneFn = deleteOne(VoaProvider, 'namespace'); - await deleteOneFn('123'); - }; - - await expect(toThrow).rejects.toThrow(PineconeBadRequestError); - await expect(toThrow).rejects.toThrow(serverError); - }); - }); }); diff --git a/src/data/__tests__/describeIndexStats.test.ts b/src/data/__tests__/describeIndexStats.test.ts index 0e667885..4b819b01 100644 --- a/src/data/__tests__/describeIndexStats.test.ts +++ b/src/data/__tests__/describeIndexStats.test.ts @@ -1,8 +1,4 @@ import { describeIndexStats } from '../describeIndexStats'; -import { - PineconeBadRequestError, - PineconeInternalServerError, -} from '../../errors'; import { VectorOperationsApi } from '../../pinecone-generated-ts-fetch'; import { VectorOperationsProvider } from '../vectorOperationsProvider'; import type { DescribeIndexStatsOperationRequest } from '../../pinecone-generated-ts-fetch'; @@ -13,7 +9,7 @@ const setupResponse = (response, isSuccess) => { ) => Promise = jest .fn() .mockImplementation(() => - isSuccess ? Promise.resolve(response) : Promise.reject({ response }) + isSuccess ? Promise.resolve(response) : Promise.reject(response) ); const VOA = { describeIndexStats: fakeDescribeIndexStats, @@ -24,9 +20,6 @@ const setupResponse = (response, isSuccess) => { const setupSuccess = (response) => { return setupResponse(response, true); }; -const setupFailure = (response) => { - return setupResponse(response, false); -}; describe('describeIndexStats', () => { test('calls the openapi describe_index_stats endpoint passing filter if provided', async () => { @@ -57,35 +50,4 @@ describe('describeIndexStats', () => { describeIndexStatsRequest: { filter: { genre: 'classical' } }, }); }); - - describe('http error mapping', () => { - test('when 500 occurs', async () => { - const { VoaProvider } = setupFailure({ - status: 500, - text: () => 'backend error message', - }); - const toThrow = async () => { - const describeIndexStatsFn = describeIndexStats(VoaProvider); - await describeIndexStatsFn({ filter: { genre: 'classical' } }); - }; - - await expect(toThrow).rejects.toThrow(PineconeInternalServerError); - }); - - test('when 400 occurs, displays server message', async () => { - const serverError = 'there has been a server error!'; - const { VoaProvider } = setupFailure({ - status: 400, - text: () => serverError, - }); - - const toThrow = async () => { - const describeIndexStatsFn = describeIndexStats(VoaProvider); - await describeIndexStatsFn({ filter: { genre: 'classical' } }); - }; - - await expect(toThrow).rejects.toThrow(PineconeBadRequestError); - await expect(toThrow).rejects.toThrow(serverError); - }); - }); }); diff --git a/src/data/__tests__/fetch.test.ts b/src/data/__tests__/fetch.test.ts index 5ac5ecd9..4f0e0996 100644 --- a/src/data/__tests__/fetch.test.ts +++ b/src/data/__tests__/fetch.test.ts @@ -1,8 +1,4 @@ import { FetchCommand } from '../fetch'; -import { - PineconeBadRequestError, - PineconeInternalServerError, -} from '../../errors'; import { VectorOperationsApi } from '../../pinecone-generated-ts-fetch'; import { VectorOperationsProvider } from '../vectorOperationsProvider'; import type { @@ -14,7 +10,7 @@ const setupResponse = (response, isSuccess) => { const fakeFetch: (req: FetchRequest) => Promise = jest .fn() .mockImplementation(() => - isSuccess ? Promise.resolve(response) : Promise.reject({ response }) + isSuccess ? Promise.resolve(response) : Promise.reject(response) ); const VOA = { fetch: fakeFetch } as VectorOperationsApi; const VoaProvider = { provide: async () => VOA } as VectorOperationsProvider; @@ -24,9 +20,6 @@ const setupResponse = (response, isSuccess) => { const setupSuccess = (response) => { return setupResponse(response, true); }; -const setupFailure = (response) => { - return setupResponse(response, false); -}; describe('fetch', () => { test('calls the openapi fetch endpoint, passing target namespace', async () => { @@ -39,33 +32,4 @@ describe('fetch', () => { namespace: 'namespace', }); }); - - describe('http error mapping', () => { - test('when 500 occurs', async () => { - const { cmd } = setupFailure({ - status: 500, - text: () => 'backend error message', - }); - - const toThrow = async () => { - await cmd.run(['1']); - }; - - await expect(toThrow).rejects.toThrow(PineconeInternalServerError); - }); - - test('when 400 occurs, displays server message', async () => { - const { cmd } = setupFailure({ - status: 400, - text: () => 'backend error message', - }); - - const toThrow = async () => { - await cmd.run(['1']); - }; - - await expect(toThrow).rejects.toThrow(PineconeBadRequestError); - await expect(toThrow).rejects.toThrow('backend error message'); - }); - }); }); diff --git a/src/data/__tests__/update.test.ts b/src/data/__tests__/update.test.ts index 0060492e..b920c825 100644 --- a/src/data/__tests__/update.test.ts +++ b/src/data/__tests__/update.test.ts @@ -1,8 +1,4 @@ import { UpdateCommand } from '../update'; -import { - PineconeBadRequestError, - PineconeInternalServerError, -} from '../../errors'; import { VectorOperationsApi } from '../../pinecone-generated-ts-fetch'; import { VectorOperationsProvider } from '../vectorOperationsProvider'; import type { UpdateOperationRequest } from '../../pinecone-generated-ts-fetch'; @@ -11,7 +7,7 @@ const setupResponse = (response, isSuccess) => { const fakeUpdate: (req: UpdateOperationRequest) => Promise = jest .fn() .mockImplementation(() => - isSuccess ? Promise.resolve(response) : Promise.reject({ response }) + isSuccess ? Promise.resolve(response) : Promise.reject(response) ); const VOA = { update: fakeUpdate } as VectorOperationsApi; const VoaProvider = { provide: async () => VOA } as VectorOperationsProvider; @@ -21,9 +17,6 @@ const setupResponse = (response, isSuccess) => { const setupSuccess = (response) => { return setupResponse(response, true); }; -const setupFailure = (response) => { - return setupResponse(response, false); -}; describe('update', () => { test('calls the openapi update endpoint, passing target namespace', async () => { @@ -53,33 +46,4 @@ describe('update', () => { }, }); }); - - describe('http error mapping', () => { - test('when 500 occurs', async () => { - const { cmd } = setupFailure({ - status: 500, - text: () => 'backend error message', - }); - - const toThrow = async () => { - await cmd.run({ id: 'fake-vector', values: [0.1, 0.2, 0.3] }); - }; - - await expect(toThrow).rejects.toThrow(PineconeInternalServerError); - }); - - test('when 400 occurs, displays server message', async () => { - const { cmd } = setupFailure({ - status: 400, - text: () => 'backend error message', - }); - - const toThrow = async () => { - await cmd.run({ id: 'fake-vector', values: [0.1, 0.2, 0.3] }); - }; - - await expect(toThrow).rejects.toThrow(PineconeBadRequestError); - await expect(toThrow).rejects.toThrow('backend error message'); - }); - }); }); diff --git a/src/data/__tests__/upsert.test.ts b/src/data/__tests__/upsert.test.ts index 95abe4ae..d4da1428 100644 --- a/src/data/__tests__/upsert.test.ts +++ b/src/data/__tests__/upsert.test.ts @@ -1,8 +1,4 @@ import { UpsertCommand } from '../upsert'; -import { - PineconeBadRequestError, - PineconeInternalServerError, -} from '../../errors'; import { VectorOperationsApi } from '../../pinecone-generated-ts-fetch'; import type { UpsertOperationRequest } from '../../pinecone-generated-ts-fetch'; import { VectorOperationsProvider } from '../vectorOperationsProvider'; @@ -11,7 +7,7 @@ const setupResponse = (response, isSuccess) => { const fakeUpsert: (req: UpsertOperationRequest) => Promise = jest .fn() .mockImplementation(() => - isSuccess ? Promise.resolve(response) : Promise.reject({ response }) + isSuccess ? Promise.resolve(response) : Promise.reject(response) ); const VOA = { upsert: fakeUpsert } as VectorOperationsApi; const VoaProvider = { provide: async () => VOA } as VectorOperationsProvider; @@ -22,9 +18,6 @@ const setupResponse = (response, isSuccess) => { const setupSuccess = (response) => { return setupResponse(response, true); }; -const setupFailure = (response) => { - return setupResponse(response, false); -}; describe('upsert', () => { test('calls the openapi upsert endpoint', async () => { @@ -40,33 +33,4 @@ describe('upsert', () => { }, }); }); - - describe('http error mapping', () => { - test('when 500 occurs', async () => { - const { cmd } = setupFailure({ - status: 500, - text: () => 'backend error message', - }); - - const toThrow = async () => { - await cmd.run([]); - }; - - await expect(toThrow).rejects.toThrow(PineconeInternalServerError); - }); - - test('when 400 occurs, displays server message', async () => { - const { cmd } = setupFailure({ - status: 400, - text: () => 'backend error message', - }); - - const toThrow = async () => { - await cmd.run([]); - }; - - await expect(toThrow).rejects.toThrow(PineconeBadRequestError); - await expect(toThrow).rejects.toThrow('backend error message'); - }); - }); }); diff --git a/src/data/deleteAll.ts b/src/data/deleteAll.ts index 6ac8e323..4a3cd288 100644 --- a/src/data/deleteAll.ts +++ b/src/data/deleteAll.ts @@ -1,4 +1,3 @@ -import { handleApiError } from '../errors'; import { VectorOperationsProvider } from './vectorOperationsProvider'; export const deleteAll = ( @@ -6,13 +5,8 @@ export const deleteAll = ( namespace: string ) => { return async (): Promise => { - try { - const api = await apiProvider.provide(); - await api._delete({ deleteRequest: { deleteAll: true, namespace } }); - return; - } catch (e) { - const err = await handleApiError(e); - throw err; - } + const api = await apiProvider.provide(); + await api._delete({ deleteRequest: { deleteAll: true, namespace } }); + return; }; }; diff --git a/src/data/deleteMany.ts b/src/data/deleteMany.ts index 7c2cbec7..34ea5aed 100644 --- a/src/data/deleteMany.ts +++ b/src/data/deleteMany.ts @@ -1,5 +1,4 @@ import { VectorOperationsProvider } from './vectorOperationsProvider'; -import { handleApiError } from '../errors'; import { buildConfigValidator } from '../validator'; import { Type } from '@sinclair/typebox'; import type { DeleteRequest } from '../pinecone-generated-ts-fetch/models/DeleteRequest'; @@ -52,12 +51,7 @@ export const deleteMany = ( requestOptions.filter = options; } - try { - const api = await apiProvider.provide(); - await api._delete({ deleteRequest: { ...requestOptions, namespace } }); - } catch (e) { - const err = await handleApiError(e); - throw err; - } + const api = await apiProvider.provide(); + await api._delete({ deleteRequest: { ...requestOptions, namespace } }); }; }; diff --git a/src/data/deleteOne.ts b/src/data/deleteOne.ts index 618fd807..23e033a9 100644 --- a/src/data/deleteOne.ts +++ b/src/data/deleteOne.ts @@ -1,5 +1,4 @@ import { VectorOperationsProvider } from './vectorOperationsProvider'; -import { handleApiError } from '../errors'; import { buildConfigValidator } from '../validator'; import { RecordIdSchema } from './types'; import type { RecordId } from './types'; @@ -20,13 +19,8 @@ export const deleteOne = ( return async (options: RecordId): Promise => { validator(options); - try { - const api = await apiProvider.provide(); - await api._delete({ deleteRequest: { ids: [options], namespace } }); - return; - } catch (e) { - const err = await handleApiError(e); - throw err; - } + const api = await apiProvider.provide(); + await api._delete({ deleteRequest: { ids: [options], namespace } }); + return; }; }; diff --git a/src/data/describeIndexStats.ts b/src/data/describeIndexStats.ts index 68664166..fa6bebc5 100644 --- a/src/data/describeIndexStats.ts +++ b/src/data/describeIndexStats.ts @@ -1,4 +1,3 @@ -import { handleApiError } from '../errors'; import { buildConfigValidator } from '../validator'; import { Type } from '@sinclair/typebox'; import { VectorOperationsProvider } from './vectorOperationsProvider'; @@ -73,30 +72,25 @@ export const describeIndexStats = (apiProvider: VectorOperationsProvider) => { validator(options); } - try { - const api = await apiProvider.provide(); - const results = await api.describeIndexStats({ - describeIndexStatsRequest: { ...options }, - }); + const api = await apiProvider.provide(); + const results = await api.describeIndexStats({ + describeIndexStatsRequest: { ...options }, + }); - const mappedResult = { - namespaces: {}, - dimension: results.dimension, - indexFullness: results.indexFullness, - totalRecordCount: results.totalVectorCount, - }; - if (results.namespaces) { - for (const key in results.namespaces) { - mappedResult.namespaces[key] = { - recordCount: results.namespaces[key].vectorCount, - }; - } + const mappedResult = { + namespaces: {}, + dimension: results.dimension, + indexFullness: results.indexFullness, + totalRecordCount: results.totalVectorCount, + }; + if (results.namespaces) { + for (const key in results.namespaces) { + mappedResult.namespaces[key] = { + recordCount: results.namespaces[key].vectorCount, + }; } - - return mappedResult; - } catch (e) { - const err = await handleApiError(e); - throw err; } + + return mappedResult; }; }; diff --git a/src/data/fetch.ts b/src/data/fetch.ts index dc34a237..a1a8b55f 100644 --- a/src/data/fetch.ts +++ b/src/data/fetch.ts @@ -1,4 +1,3 @@ -import { handleApiError } from '../errors'; import { buildConfigValidator } from '../validator'; import { VectorOperationsProvider } from './vectorOperationsProvider'; import { RecordIdSchema } from './types'; @@ -38,20 +37,15 @@ export class FetchCommand { async run(ids: FetchOptions): Promise> { this.validator(ids); - try { - const api = await this.apiProvider.provide(); - const response = await api.fetch({ ids: ids, namespace: this.namespace }); - - // My testing shows that in reality vectors and namespace are - // never undefined even when there are no records returned. So these - // default values are needed only to satisfy the typescript compiler. - return { - records: response.vectors ? response.vectors : {}, - namespace: response.namespace ? response.namespace : '', - } as FetchResponse; - } catch (e) { - const err = await handleApiError(e); - throw err; - } + const api = await this.apiProvider.provide(); + const response = await api.fetch({ ids: ids, namespace: this.namespace }); + + // My testing shows that in reality vectors and namespace are + // never undefined even when there are no records returned. So these + // default values are needed only to satisfy the typescript compiler. + return { + records: response.vectors ? response.vectors : {}, + namespace: response.namespace ? response.namespace : '', + } as FetchResponse; } } diff --git a/src/data/projectIdSingleton.ts b/src/data/projectIdSingleton.ts index 1573117f..47d7abbd 100644 --- a/src/data/projectIdSingleton.ts +++ b/src/data/projectIdSingleton.ts @@ -1,8 +1,7 @@ import { PineconeUnexpectedResponseError, - PineconeConfigurationError, - PineconeUnknownRequestFailure, mapHttpStatusError, + PineconeConnectionError, } from '../errors'; import type { PineconeConfiguration } from './types'; import { buildUserAgent, getFetch } from '../utils'; @@ -29,13 +28,7 @@ export const ProjectIdSingleton = (function () { // will occur if the connection fails due to invalid environment configuration provided by the user. This is // different from server errors handled below because the client is unable to make contact with a Pinecone // server at all without a valid environment value. - if (e instanceof TypeError) { - throw new PineconeConfigurationError( - `A network error occured while attempting to connect to ${url}. Are you sure you passed the correct environment? Please check your configuration values and try again. Visit https://status.pinecone.io for overall service health information.` - ); - } else { - throw new PineconeUnknownRequestFailure(url, e); - } + throw new PineconeConnectionError(e, url); } if (response.status >= 400) { diff --git a/src/data/query.ts b/src/data/query.ts index 1b2d3315..5bd223f2 100644 --- a/src/data/query.ts +++ b/src/data/query.ts @@ -1,4 +1,3 @@ -import { handleApiError } from '../errors'; import { buildConfigValidator } from '../validator'; import { RecordIdSchema, @@ -142,20 +141,15 @@ export class QueryCommand { async run(query: QueryOptions): Promise> { this.validator(query); - try { - const api = await this.apiProvider.provide(); - const results = await api.query({ - queryRequest: { ...query, namespace: this.namespace }, - }); - const matches = results.matches ? results.matches : []; - - return { - matches: matches as Array>, - namespace: this.namespace, - }; - } catch (e) { - const err = await handleApiError(e); - throw err; - } + const api = await this.apiProvider.provide(); + const results = await api.query({ + queryRequest: { ...query, namespace: this.namespace }, + }); + const matches = results.matches ? results.matches : []; + + return { + matches: matches as Array>, + namespace: this.namespace, + }; } } diff --git a/src/data/update.ts b/src/data/update.ts index 05d1633c..fc7ecc90 100644 --- a/src/data/update.ts +++ b/src/data/update.ts @@ -1,4 +1,3 @@ -import { handleApiError } from '../errors'; import { buildConfigValidator } from '../validator'; import { Type } from '@sinclair/typebox'; import { VectorOperationsProvider } from './vectorOperationsProvider'; @@ -70,15 +69,10 @@ export class UpdateCommand { setMetadata: options['metadata'], }; - try { - const api = await this.apiProvider.provide(); - await api.update({ - updateRequest: { ...requestOptions, namespace: this.namespace }, - }); - return; - } catch (e) { - const err = await handleApiError(e); - throw err; - } + const api = await this.apiProvider.provide(); + await api.update({ + updateRequest: { ...requestOptions, namespace: this.namespace }, + }); + return; } } diff --git a/src/data/upsert.ts b/src/data/upsert.ts index 4f2c9032..bea31d7e 100644 --- a/src/data/upsert.ts +++ b/src/data/upsert.ts @@ -1,4 +1,3 @@ -import { handleApiError } from '../errors'; import { buildConfigValidator } from '../validator'; import { PineconeRecordSchema } from './types'; import { Type } from '@sinclair/typebox'; @@ -22,18 +21,13 @@ export class UpsertCommand { async run(records: Array>): Promise { this.validator(records); - try { - const api = await this.apiProvider.provide(); - await api.upsert({ - upsertRequest: { - vectors: records as Array, - namespace: this.namespace, - }, - }); - return; - } catch (e) { - const err = await handleApiError(e); - throw err; - } + const api = await this.apiProvider.provide(); + await api.upsert({ + upsertRequest: { + vectors: records as Array, + namespace: this.namespace, + }, + }); + return; } } diff --git a/src/data/vectorOperationsProvider.ts b/src/data/vectorOperationsProvider.ts index 859f811b..efc0ebc0 100644 --- a/src/data/vectorOperationsProvider.ts +++ b/src/data/vectorOperationsProvider.ts @@ -6,6 +6,7 @@ import { } from '../pinecone-generated-ts-fetch'; import { queryParamsStringify, buildUserAgent, getFetch } from '../utils'; import { ProjectIdSingleton } from './projectIdSingleton'; +import { middleware } from '../utils/middleware'; const basePath = (config: PineconeConfiguration, indexName: string) => `https://${indexName}-${config.projectId}.svc.${config.environment}.pinecone.io`; @@ -52,6 +53,7 @@ export class VectorOperationsProvider { 'User-Agent': buildUserAgent(false), }, fetchApi: getFetch(config), + middleware, }; const indexConfiguration = new Configuration(indexConfigurationParameters); diff --git a/src/errors/base.ts b/src/errors/base.ts index c639f102..0c657176 100644 --- a/src/errors/base.ts +++ b/src/errors/base.ts @@ -1,5 +1,8 @@ export class BasePineconeError extends Error { - constructor(message?: string) { + /** The underlying error, if any. */ + cause?: Error; + + constructor(message?: string, cause?: Error) { super(message); // Set the prototype explicitly to ensure instanceof works correctly @@ -10,7 +13,7 @@ export class BasePineconeError extends Error { Error.captureStackTrace(this, new.target); } - // set the name property this.name = this.constructor.name; + this.cause = cause; } } diff --git a/src/errors/config.ts b/src/errors/config.ts index c92078be..3e9585bf 100644 --- a/src/errors/config.ts +++ b/src/errors/config.ts @@ -58,18 +58,3 @@ export class PineconeEnvironmentVarsNotSupportedError extends BasePineconeError this.name = 'PineconeEnvironmentVarsNotSupportedError'; } } - -/** - * This error reflects a problem while fetching project id. It is not expected to ever occur. - * - * If you encounter this error, please [file an issue](https://github.com/pinecone-io/pinecone-ts-client/issues) so we can investigate. - */ -export class PineconeUnknownRequestFailure extends BasePineconeError { - constructor(url: string, underlyingError: Error) { - const message = `Something went wrong while attempting to call ${url}. Please check your configuration and try again later. Underlying error was ${JSON.stringify( - underlyingError.message - )}`; - super(message); - this.name = 'PineconeUnknownRequestFailure'; - } -} diff --git a/src/errors/handling.ts b/src/errors/handling.ts index 24669e04..3efb297d 100644 --- a/src/errors/handling.ts +++ b/src/errors/handling.ts @@ -3,22 +3,6 @@ import { mapHttpStatusError } from './http'; import { PineconeConnectionError } from './request'; import type { ResponseError } from '../pinecone-generated-ts-fetch'; -// We want to check for FetchError in a consistent way, then continue to other customizable error handling -// for all other API errors. The FetchError type arises when someone has configured the client with an invalid -// environment value; in this case no connection is ever made to a server so there's no response status code or -// body contents with information about the error. -/** @internal */ -export const handleFetchError = async ( - e: unknown, - handleResponseError: (e: ResponseError) => Promise -): Promise => { - if (e instanceof Error && e.name === 'FetchError') { - return new PineconeConnectionError(); - } else { - return await handleResponseError(e as ResponseError); - } -}; - /** @internal */ export const handleApiError = async ( e: unknown, @@ -27,20 +11,28 @@ export const handleApiError = async ( rawMessageText: string ) => Promise ): Promise => { - return await handleFetchError( - e, - async (responseError: ResponseError): Promise => { - const rawMessage = await extractMessage(responseError); - const statusCode = responseError.response.status; - const message = customMessage - ? await customMessage(statusCode, rawMessage) - : rawMessage; + if (e instanceof Error && e.name === 'ResponseError') { + const responseError = e as ResponseError; + const rawMessage = await extractMessage(responseError); + const statusCode = responseError.response.status; + const message = customMessage + ? await customMessage(statusCode, rawMessage) + : rawMessage; - return mapHttpStatusError({ - status: responseError.response.status, - url: responseError.response.url, - message: message, - }); - } - ); + return mapHttpStatusError({ + status: responseError.response.status, + url: responseError.response.url, + message: message, + }); + } else if (e instanceof PineconeConnectionError) { + // If we've already wrapped this error, just return it + return e; + } else { + // There seem to be some situations where "e instanceof Error" is erroneously + // false (perhaps the custom errors emitted by cross-fetch do not extend Error?) + // but we can still cast it to an Error type because all we're going to do + // with it is store off a reference to whatever it is under the "cause" + const err = e as Error; + return new PineconeConnectionError(err); + } }; diff --git a/src/errors/http.ts b/src/errors/http.ts index 0e42c809..4c8ed71b 100644 --- a/src/errors/http.ts +++ b/src/errors/http.ts @@ -55,9 +55,7 @@ export class PineconeNotFoundError extends BasePineconeError { constructor(failedRequest: FailedRequestInfo) { const { url, message } = failedRequest; if (url) { - super( - `A call to ${url} returned HTTP status 404. ${message ? message : ''}` - ); + super(`A call to ${url} returned HTTP status 404.`); } else if (message) { super(message); } else { diff --git a/src/errors/index.ts b/src/errors/index.ts index 822799cc..14f1f254 100644 --- a/src/errors/index.ts +++ b/src/errors/index.ts @@ -2,10 +2,10 @@ export { PineconeConfigurationError, PineconeUnexpectedResponseError, PineconeEnvironmentVarsNotSupportedError, - PineconeUnknownRequestFailure, } from './config'; export * from './http'; -export { PineconeConnectionError } from './request'; +export { PineconeConnectionError, PineconeRequestError } from './request'; +export { BasePineconeError } from './base'; export { PineconeArgumentError } from './validation'; export { extractMessage } from './utils'; export { handleApiError } from './handling'; diff --git a/src/errors/request.ts b/src/errors/request.ts index 91912411..0d6168c7 100644 --- a/src/errors/request.ts +++ b/src/errors/request.ts @@ -1,4 +1,5 @@ import { BasePineconeError } from './base'; +import type { ErrorContext } from '../pinecone-generated-ts-fetch'; /** * This error is thrown when the client attempts to make a @@ -8,13 +9,56 @@ import { BasePineconeError } from './base'; * - Incorrect configuration of the client. The client builds its connection url using values supplied in configuration, so if these values are incorrect the request will not reach a Pinecone server. * - An outage of Pinecone's APIs. See [Pinecone's status page](https://status.pinecone.io/) to find out whether there is an ongoing incident. * + * The `cause` property will contain a reference to the underlying error. Inspect its value to find out more about the root cause of the error. + * ``` + * import { Pinecone } from '@pinecone-database/pinecone'; + * + * const p = new Pinecone({ apiKey: 'api-key-value', environment: 'wrong-environment' }) + * + * try { + * await p.listIndexes(); + * } catch (e) { + * console.log(e.name); // PineconeConnectionError + * console.log(e.cause); // Error [FetchError]: The request failed and the interceptors did not return an alternative response + * console.log(e.cause.cause); // TypeError: fetch failed + * console.log(e.cause.cause.cause); // Error: getaddrinfo ENOTFOUND controller.wrong-environment.pinecone.io + * } + * ``` + * * @see [Pinecone's status page](https://status.pinecone.io/) * */ export class PineconeConnectionError extends BasePineconeError { - constructor() { + constructor(e: Error, url?: string) { + let urlMessage = ''; + if (url) { + urlMessage = ` while calling ${url}`; + } + super( - 'Request failed to reach Pinecone. Verify you have the correct environment, project id, and index name configured.' + `Request failed to reach Pinecone${urlMessage}. This can occur for reasons such as incorrect configuration (environment, project id, index name), network problems that prevent the request from being completed, or a Pinecone API outage. Check your client configuration, check your network connection, and visit https://status.pinecone.io/ to see whether any outages are ongoing.`, + e ); this.name = 'PineconeConnectionError'; } } + +/** + * This error is thrown any time a request to the Pinecone API fails. + * + * The `cause` property will contain a reference to the underlying error. Inspect its value to find out more about the root cause. + */ +export class PineconeRequestError extends BasePineconeError { + constructor(context: ErrorContext) { + if (context.response) { + super( + `Request failed during a call to ${context.init.method} ${context.url} with status ${context.response.status}`, + context.error as Error + ); + } else { + super( + `Request failed during a call to ${context.init.method} ${context.url}`, + context.error as Error + ); + } + } +} diff --git a/src/integration/control/configureIndex.test.ts b/src/integration/control/configureIndex.test.ts new file mode 100644 index 00000000..5519bbc9 --- /dev/null +++ b/src/integration/control/configureIndex.test.ts @@ -0,0 +1,133 @@ +import { BasePineconeError } from '../../errors'; +import { Pinecone } from '../../index'; +import { randomIndexName, waitUntilReady } from '../test-helpers'; + +describe('configure index', () => { + let indexName; + let pinecone: Pinecone; + + beforeEach(async () => { + pinecone = new Pinecone(); + indexName = randomIndexName('configureIndex'); + + await pinecone.createIndex({ + name: indexName, + dimension: 5, + waitUntilReady: true, + podType: 'p1.x1', + }); + }); + + afterEach(async () => { + await pinecone.deleteIndex(indexName); + }); + + describe('error handling', () => { + test('configure index with invalid index name', async () => { + try { + await pinecone.configureIndex('non-existent-index', { replicas: 2 }); + } catch (e) { + const err = e as BasePineconeError; + expect(err.name).toEqual('PineconeNotFoundError'); + expect(err.message).toEqual( + `A call to https://controller.${process.env.PINECONE_ENVIRONMENT}.pinecone.io/databases/non-existent-index returned HTTP status 404.` + ); + } + }); + + test('configure index when exceeding quota', async () => { + try { + await pinecone.configureIndex(indexName, { replicas: 20 }); + } catch (e) { + const err = e as BasePineconeError; + expect(err.name).toEqual('PineconeBadRequestError'); + expect(err.message).toContain('The index exceeds the project quota'); + expect(err.message).toContain( + 'Upgrade your account or change the project settings to increase the quota.' + ); + } + }); + }); + + describe('scaling replicas', () => { + test('up and down', async () => { + const description = await pinecone.describeIndex(indexName); + expect(description.database?.replicas).toEqual(1); + + // Scale up + await pinecone.configureIndex(indexName, { replicas: 2 }); + const description2 = await pinecone.describeIndex(indexName); + expect(description2.database?.replicas).toEqual(2); + + await waitUntilReady(indexName); + + // Scale down + await pinecone.configureIndex(indexName, { replicas: 1 }); + const description3 = await pinecone.describeIndex(indexName); + expect(description3.database?.replicas).toEqual(1); + }); + }); + + describe('scaling pod type', () => { + test('scaling podType: changing base pod type', async () => { + // Verify the starting state + const description = await pinecone.describeIndex(indexName); + expect(description.database?.podType).toEqual('p1.x1'); + + try { + // Try to change the base pod type + await pinecone.configureIndex(indexName, { podType: 'p2.x1' }); + } catch (e) { + const err = e as BasePineconeError; + expect(err.name).toEqual('PineconeBadRequestError'); + expect(err.message).toContain( + 'updating base pod type is not supported' + ); + } + }); + + test('scaling podType: size increase', async () => { + // Verify the starting state + const description = await pinecone.describeIndex(indexName); + expect(description.database?.podType).toEqual('p1.x1'); + + await pinecone.configureIndex(indexName, { podType: 'p1.x2' }); + const description2 = await pinecone.describeIndex(indexName); + expect(description2.database?.podType).toEqual('p1.x2'); + }); + + test('scaling podType: size down', async () => { + // Verify the starting state + const description = await pinecone.describeIndex(indexName); + expect(description.database?.podType).toEqual('p1.x1'); + + // Size up + await pinecone.configureIndex(indexName, { podType: 'p1.x2' }); + const description2 = await pinecone.describeIndex(indexName); + expect(description2.database?.podType).toEqual('p1.x2'); + + await waitUntilReady(indexName); + + try { + // try to size down + await pinecone.configureIndex(indexName, { podType: 'p1.x1' }); + const description3 = await pinecone.describeIndex(indexName); + expect(description3.database?.podType).toEqual('p1.x1'); + } catch (e) { + const err = e as BasePineconeError; + + if (err.name === 'PineconeBadRequestError') { + expect(err.message).toContain( + 'scaling down pod type is not supported' + ); + } else if (err.name === 'PineconeInternalServerError') { + // noop. Seems like the API is sometimes returns 500 when scaling up + // and down in quick succession. But it's not a client issue so I + // don't want to fail the test in that case. + } else { + throw err; + } + } + }); + }); +}); diff --git a/src/integration/control/createIndex.test.ts b/src/integration/control/createIndex.test.ts new file mode 100644 index 00000000..02580dd1 --- /dev/null +++ b/src/integration/control/createIndex.test.ts @@ -0,0 +1,124 @@ +import { PineconeNotFoundError } from '../../errors'; +import { Pinecone } from '../../index'; +import { randomIndexName } from '../test-helpers'; + +describe('create index', () => { + let indexName; + let pinecone: Pinecone; + + beforeEach(async () => { + indexName = randomIndexName('createIndex'); + pinecone = new Pinecone(); + }); + + describe('happy path', () => { + afterEach(async () => { + await pinecone.deleteIndex(indexName); + }); + + test('simple create', async () => { + await pinecone.createIndex({ + name: indexName, + dimension: 5, + }); + const description = await pinecone.describeIndex(indexName); + expect(description.database?.name).toEqual(indexName); + expect(description.database?.dimension).toEqual(5); + expect(description.database?.metric).toEqual('cosine'); + expect(description.database?.pods).toEqual(1); + expect(description.database?.replicas).toEqual(1); + expect(description.database?.shards).toEqual(1); + expect(description.status?.host).toBeDefined(); + }); + + test('create with optional properties', async () => { + await pinecone.createIndex({ + name: indexName, + dimension: 5, + metric: 'euclidean', + replicas: 2, + podType: 'p1.x2', + }); + + const description = await pinecone.describeIndex(indexName); + expect(description.database?.name).toEqual(indexName); + expect(description.database?.dimension).toEqual(5); + expect(description.database?.metric).toEqual('euclidean'); + expect(description.database?.pods).toEqual(2); + expect(description.database?.replicas).toEqual(2); + expect(description.database?.shards).toEqual(1); + expect(description.status?.host).toBeDefined(); + expect(description.status?.state).toEqual('Initializing'); + }); + + test('create with utility prop: waitUntilReady', async () => { + await pinecone.createIndex({ + name: indexName, + dimension: 5, + waitUntilReady: true, + }); + + const description = await pinecone.describeIndex(indexName); + expect(description.database?.name).toEqual(indexName); + expect(description.status?.state).toEqual('Ready'); + }); + + test('create with utility prop: suppressConflicts', async () => { + await pinecone.createIndex({ + name: indexName, + dimension: 5, + }); + await pinecone.createIndex({ + name: indexName, + dimension: 5, + suppressConflicts: true, + }); + + const description = await pinecone.describeIndex(indexName); + expect(description.database?.name).toEqual(indexName); + }); + }); + + describe('error cases', () => { + test('create index with invalid index name', async () => { + try { + await pinecone.createIndex({ + name: indexName + '-', + dimension: 5, + }); + } catch (e) { + const err = e as PineconeNotFoundError; + expect(err.name).toEqual('PineconeBadRequestError'); + expect(err.message).toContain('alphanumeric characters'); + } + }); + + test('insufficient quota', async () => { + try { + await pinecone.createIndex({ + name: indexName, + dimension: 5, + replicas: 20, + }); + } catch (e) { + const err = e as PineconeNotFoundError; + expect(err.name).toEqual('PineconeBadRequestError'); + expect(err.message).toContain('exceeds the project quota'); + } + }); + + test('create from non-existent collection', async () => { + try { + await pinecone.createIndex({ + name: indexName, + dimension: 5, + sourceCollection: 'non-existent-collection', + }); + } catch (e) { + const err = e as PineconeNotFoundError; + expect(err.name).toEqual('PineconeBadRequestError'); + expect(err.message).toContain('failed to fetch source collection'); + } + }); + }); +}); diff --git a/src/integration/control/describeIndex.test.ts b/src/integration/control/describeIndex.test.ts new file mode 100644 index 00000000..71144b3d --- /dev/null +++ b/src/integration/control/describeIndex.test.ts @@ -0,0 +1,45 @@ +import { PineconeNotFoundError } from '../../errors'; +import { Pinecone } from '../../index'; +import { randomIndexName } from '../test-helpers'; + +describe('describe index', () => { + let indexName; + let pinecone: Pinecone; + + beforeEach(async () => { + indexName = randomIndexName('describeIndex'); + pinecone = new Pinecone(); + + await pinecone.createIndex({ + name: indexName, + dimension: 5, + }); + }); + + afterEach(async () => { + await pinecone.deleteIndex(indexName); + }); + + test('describe index', async () => { + const description = await pinecone.describeIndex(indexName); + expect(description.database?.name).toEqual(indexName); + expect(description.database?.dimension).toEqual(5); + expect(description.database?.metric).toEqual('cosine'); + expect(description.database?.pods).toEqual(1); + expect(description.database?.replicas).toEqual(1); + expect(description.database?.shards).toEqual(1); + expect(description.status?.host).toBeDefined(); + }); + + test('describe index with invalid index name', async () => { + try { + await pinecone.describeIndex('non-existent-index'); + } catch (e) { + const err = e as PineconeNotFoundError; + expect(err.name).toEqual('PineconeNotFoundError'); + expect(err.message).toEqual( + `A call to https://controller.${process.env.PINECONE_ENVIRONMENT}.pinecone.io/databases/non-existent-index returned HTTP status 404.` + ); + } + }); +}); diff --git a/src/integration/control/listIndexes.test.ts b/src/integration/control/listIndexes.test.ts new file mode 100644 index 00000000..183c6a43 --- /dev/null +++ b/src/integration/control/listIndexes.test.ts @@ -0,0 +1,29 @@ +import { Pinecone } from '../../index'; +import { randomIndexName } from '../test-helpers'; + +describe('list indexes', () => { + let indexName; + let pinecone: Pinecone; + + beforeEach(async () => { + indexName = randomIndexName('listIndexes'); + pinecone = new Pinecone(); + + await pinecone.createIndex({ + name: indexName, + dimension: 5, + }); + }); + + afterEach(async () => { + await pinecone.deleteIndex(indexName); + }); + + test('list indexes', async () => { + const indexes = await pinecone.listIndexes(); + expect(indexes).toBeDefined(); + expect(indexes.length).toBeGreaterThan(0); + + expect(indexes.map((i) => i.name)).toContain(indexName); + }); +}); diff --git a/src/integration/data/deleteMany.test.ts b/src/integration/data/deleteMany.test.ts index 8debea58..50f490ec 100644 --- a/src/integration/data/deleteMany.test.ts +++ b/src/integration/data/deleteMany.test.ts @@ -5,7 +5,7 @@ describe('deleteMany', () => { const INDEX_NAME = 'ts-integration'; let pinecone: Pinecone, index: Index, ns: Index, namespace: string; - beforeAll(async () => { + beforeEach(async () => { pinecone = new Pinecone(); await pinecone.createIndex({ diff --git a/src/integration/data/query.test.ts b/src/integration/data/query.test.ts index 2da5375f..2791fbe4 100644 --- a/src/integration/data/query.test.ts +++ b/src/integration/data/query.test.ts @@ -5,7 +5,7 @@ describe('query', () => { const INDEX_NAME = 'ts-integration'; let pinecone: Pinecone, index: Index, ns: Index, namespace: string; - beforeAll(async () => { + beforeEach(async () => { pinecone = new Pinecone(); await pinecone.createIndex({ diff --git a/src/integration/errorHandling.test.ts b/src/integration/errorHandling.test.ts new file mode 100644 index 00000000..29eb6925 --- /dev/null +++ b/src/integration/errorHandling.test.ts @@ -0,0 +1,91 @@ +import { PineconeConnectionError } from '../errors'; +import { Pinecone } from '../index'; + +describe('Error handling', () => { + describe('when environment is wrong', () => { + test('calling control plane', async () => { + const p = new Pinecone({ + apiKey: process.env.PINECONE_API_KEY || '', + environment: 'wrong-environment', + }); + + try { + await p.listIndexes(); + } catch (e) { + const err = e as PineconeConnectionError; + expect(err.name).toEqual('PineconeConnectionError'); + expect(err.message).toEqual( + 'Request failed to reach Pinecone. This can occur for reasons such as incorrect configuration (environment, project id, index name), network problems that prevent the request from being completed, or a Pinecone API outage. Check your client configuration, check your network connection, and visit https://status.pinecone.io/ to see whether any outages are ongoing.' + ); + // @ts-ignore + expect(err.cause.name).toEqual('TypeError'); + // @ts-ignore + expect(err.cause.message).toEqual('fetch failed'); + // @ts-ignore + expect(err.cause.cause.name).toEqual('Error'); + //@ts-ignore + expect(err.cause.cause.message).toEqual( + 'getaddrinfo ENOTFOUND controller.wrong-environment.pinecone.io' + ); + } + }); + + test('calling data plane', async () => { + const p = new Pinecone({ + apiKey: 'api-key-2', + environment: 'wrong-environment2', + }); + + try { + await p.index('foo-index').query({ topK: 10, id: '1' }); + } catch (e) { + const err = e as PineconeConnectionError; + expect(err.name).toEqual('PineconeConnectionError'); + expect(err.message).toEqual( + 'Request failed to reach Pinecone while calling https://controller.wrong-environment2.pinecone.io/actions/whoami. This can occur for reasons such as incorrect configuration (environment, project id, index name), network problems that prevent the request from being completed, or a Pinecone API outage. Check your client configuration, check your network connection, and visit https://status.pinecone.io/ to see whether any outages are ongoing.' + ); + } + }); + + describe('when network error occurs', () => { + let p; + beforeEach(() => { + p = new Pinecone({ + apiKey: process.env.PINECONE_API_KEY || '', + environment: process.env.PINECONE_ENVIRONMENT || '', + fetchApi: async () => { + throw new Error('network failure'); + }, + }); + }); + + test('calling control plane', async () => { + try { + await p.listIndexes(); + } catch (e) { + const err = e as PineconeConnectionError; + expect(err.name).toEqual('PineconeConnectionError'); + expect(err.message).toEqual( + 'Request failed to reach Pinecone. This can occur for reasons such as incorrect configuration (environment, project id, index name), network problems that prevent the request from being completed, or a Pinecone API outage. Check your client configuration, check your network connection, and visit https://status.pinecone.io/ to see whether any outages are ongoing.' + ); + // @ts-ignore + expect(err.cause.name).toEqual('Error'); + // @ts-ignore + expect(err.cause.message).toEqual('network failure'); + } + }); + + test('calling data plane', async () => { + try { + await p.index('foo-index').query({ topK: 10, id: '1' }); + } catch (e) { + const err = e as PineconeConnectionError; + expect(err.name).toEqual('PineconeConnectionError'); + expect(err.message).toEqual( + `Request failed to reach Pinecone while calling https://controller.${process.env.PINECONE_ENVIRONMENT}.pinecone.io/actions/whoami. This can occur for reasons such as incorrect configuration (environment, project id, index name), network problems that prevent the request from being completed, or a Pinecone API outage. Check your client configuration, check your network connection, and visit https://status.pinecone.io/ to see whether any outages are ongoing.` + ); + } + }); + }); + }); +}); diff --git a/src/integration/test-helpers.ts b/src/integration/test-helpers.ts index a6586a80..3d862ef3 100644 --- a/src/integration/test-helpers.ts +++ b/src/integration/test-helpers.ts @@ -1,4 +1,5 @@ import type { PineconeRecord, RecordSparseValues } from '../index'; +import { Pinecone } from '../index'; export const randomString = (length) => { const characters = @@ -50,3 +51,24 @@ export const generateSparseValues = (dimension: number): RecordSparseValues => { const sparseValues: RecordSparseValues = { indices, values }; return sparseValues; }; + +export const randomIndexName = (testName: string): string => { + return `it-${process.env.TEST_ENV}-${testName}-${randomString(8)}` + .toLowerCase() + .slice(0, 45); +}; + +export const sleep = async (ms) => { + return new Promise((resolve) => setTimeout(resolve, ms)); +}; + +export const waitUntilReady = async (indexName: string) => { + const p = new Pinecone(); + const sleepIntervalMs = 1000; + + let description = await p.describeIndex(indexName); + while (description.status?.state !== 'Ready') { + await sleep(sleepIntervalMs); + description = await p.describeIndex(indexName); + } +}; diff --git a/src/pinecone.ts b/src/pinecone.ts index 5695bb4a..b0c8c4a3 100644 --- a/src/pinecone.ts +++ b/src/pinecone.ts @@ -23,6 +23,7 @@ import { PineconeConfigurationError, PineconeEnvironmentVarsNotSupportedError, } from './errors'; +import { middleware } from './utils/middleware'; import { Index, PineconeConfigurationSchema } from './data'; import { buildValidator } from './validator'; import { queryParamsStringify, buildUserAgent, getFetch } from './utils'; @@ -124,6 +125,7 @@ export class Pinecone { 'User-Agent': buildUserAgent(false), }, fetchApi: getFetch(options), + middleware, }; const api = new IndexOperationsApi(new ApiConfiguration(apiConfig)); @@ -496,14 +498,4 @@ export class Pinecone { Index(indexName: string) { return this.index(indexName); } - - /** @hidden */ - __curlStarter() { - // Every endpoint is going to have a different path and expect different data (in the case of POST requests), - // but this is a good starting point for users to see how to use curl to interact with the REST API. - console.log('Example curl command to list indexes: '); - console.log( - `curl "https://controller.${this.config.environment}.pinecone.io/databases" -H "Api-Key: ${this.config.apiKey}" -H "Accept: application/json"` - ); - } } diff --git a/src/utils/middleware.ts b/src/utils/middleware.ts new file mode 100644 index 00000000..82da3337 --- /dev/null +++ b/src/utils/middleware.ts @@ -0,0 +1,24 @@ +import { ResponseError } from '../pinecone-generated-ts-fetch'; +import { handleApiError } from '../errors'; + +export const middleware = [ + { + onError: async (context) => { + const err = await handleApiError(context.error); + throw err; + }, + + post: async (context) => { + const { response } = context; + + if (response.status >= 200 && response.status < 300) { + return response; + } else { + const err = await handleApiError( + new ResponseError(response, 'Response returned an error') + ); + throw err; + } + }, + }, +]; diff --git a/src/utils/testHelper.ts b/src/utils/testHelper.ts new file mode 100644 index 00000000..98b147e3 --- /dev/null +++ b/src/utils/testHelper.ts @@ -0,0 +1,11 @@ +import { ResponseError } from '../pinecone-generated-ts-fetch'; + +export const responseError = (status: number, message: string) => { + return new ResponseError( + { + status, + text: async () => message, + } as Response, + 'oops' + ); +}; From 16ff1449cfa61107a5e11088b7162bf567b96efc Mon Sep 17 00:00:00 2001 From: Jen Hamon Date: Fri, 6 Oct 2023 17:52:46 -0400 Subject: [PATCH 2/7] For now, run only one integration test build at a time --- .github/workflows/pr.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 86ff3567..b15549d1 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -41,6 +41,8 @@ jobs: name: Run integration tests runs-on: ubuntu-latest strategy: + max-parallel: 1 + fail-fast: false matrix: jest_env: ['node', 'edge'] steps: From 0a1d8bfe4440fe59d21502e4dd6d9b37f92bd279 Mon Sep 17 00:00:00 2001 From: Jen Hamon Date: Fri, 6 Oct 2023 18:43:02 -0400 Subject: [PATCH 3/7] Loosen up error messages to account for platform-specific differences --- jest.config.integration-node.js | 1 + src/integration/control/configureIndex.test.ts | 3 --- src/integration/errorHandling.test.ts | 11 +---------- src/integration/test-helpers.ts | 2 +- 4 files changed, 3 insertions(+), 14 deletions(-) diff --git a/jest.config.integration-node.js b/jest.config.integration-node.js index 5e6a12fb..e948a6cf 100644 --- a/jest.config.integration-node.js +++ b/jest.config.integration-node.js @@ -2,6 +2,7 @@ const config = require('./jest.config'); module.exports = { ...config, + reporters: [['github-actions', { silent: false }], 'default'], setupFilesAfterEnv: ['./utils/globalIntegrationTestSetup.ts'], testPathIgnorePatterns: [], testEnvironment: 'node', diff --git a/src/integration/control/configureIndex.test.ts b/src/integration/control/configureIndex.test.ts index 5519bbc9..1c74074b 100644 --- a/src/integration/control/configureIndex.test.ts +++ b/src/integration/control/configureIndex.test.ts @@ -29,9 +29,6 @@ describe('configure index', () => { } catch (e) { const err = e as BasePineconeError; expect(err.name).toEqual('PineconeNotFoundError'); - expect(err.message).toEqual( - `A call to https://controller.${process.env.PINECONE_ENVIRONMENT}.pinecone.io/databases/non-existent-index returned HTTP status 404.` - ); } }); diff --git a/src/integration/errorHandling.test.ts b/src/integration/errorHandling.test.ts index 29eb6925..a2a31b3a 100644 --- a/src/integration/errorHandling.test.ts +++ b/src/integration/errorHandling.test.ts @@ -17,16 +17,7 @@ describe('Error handling', () => { expect(err.message).toEqual( 'Request failed to reach Pinecone. This can occur for reasons such as incorrect configuration (environment, project id, index name), network problems that prevent the request from being completed, or a Pinecone API outage. Check your client configuration, check your network connection, and visit https://status.pinecone.io/ to see whether any outages are ongoing.' ); - // @ts-ignore - expect(err.cause.name).toEqual('TypeError'); - // @ts-ignore - expect(err.cause.message).toEqual('fetch failed'); - // @ts-ignore - expect(err.cause.cause.name).toEqual('Error'); - //@ts-ignore - expect(err.cause.cause.message).toEqual( - 'getaddrinfo ENOTFOUND controller.wrong-environment.pinecone.io' - ); + expect(err.cause).toBeDefined(); } }); diff --git a/src/integration/test-helpers.ts b/src/integration/test-helpers.ts index 3d862ef3..5ca6f59d 100644 --- a/src/integration/test-helpers.ts +++ b/src/integration/test-helpers.ts @@ -53,7 +53,7 @@ export const generateSparseValues = (dimension: number): RecordSparseValues => { }; export const randomIndexName = (testName: string): string => { - return `it-${process.env.TEST_ENV}-${testName}-${randomString(8)}` + return `${process.env.TEST_ENV}-${testName}-${randomString(8)}` .toLowerCase() .slice(0, 45); }; From 79643ab0d11f3b80fc79a308dc0acb9a10d3b2dc Mon Sep 17 00:00:00 2001 From: Jen Hamon Date: Sat, 7 Oct 2023 13:41:07 -0400 Subject: [PATCH 4/7] Remove assertion about index state --- src/integration/control/createIndex.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/integration/control/createIndex.test.ts b/src/integration/control/createIndex.test.ts index 02580dd1..7975815b 100644 --- a/src/integration/control/createIndex.test.ts +++ b/src/integration/control/createIndex.test.ts @@ -48,7 +48,6 @@ describe('create index', () => { expect(description.database?.replicas).toEqual(2); expect(description.database?.shards).toEqual(1); expect(description.status?.host).toBeDefined(); - expect(description.status?.state).toEqual('Initializing'); }); test('create with utility prop: waitUntilReady', async () => { From 8d1c351bef0523e8764909200a8fbc8b5ab80471 Mon Sep 17 00:00:00 2001 From: Jen Hamon Date: Sat, 7 Oct 2023 14:00:08 -0400 Subject: [PATCH 5/7] Adjust configureIndex tests --- src/integration/control/configureIndex.test.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/integration/control/configureIndex.test.ts b/src/integration/control/configureIndex.test.ts index 1c74074b..cd05f618 100644 --- a/src/integration/control/configureIndex.test.ts +++ b/src/integration/control/configureIndex.test.ts @@ -15,6 +15,7 @@ describe('configure index', () => { dimension: 5, waitUntilReady: true, podType: 'p1.x1', + replicas: 2, }); }); @@ -47,18 +48,19 @@ describe('configure index', () => { }); describe('scaling replicas', () => { - test('up and down', async () => { + test('scaling up', async () => { const description = await pinecone.describeIndex(indexName); - expect(description.database?.replicas).toEqual(1); + expect(description.database?.replicas).toEqual(2); - // Scale up - await pinecone.configureIndex(indexName, { replicas: 2 }); + await pinecone.configureIndex(indexName, { replicas: 3 }); const description2 = await pinecone.describeIndex(indexName); - expect(description2.database?.replicas).toEqual(2); + expect(description2.database?.replicas).toEqual(3); + }); - await waitUntilReady(indexName); + test('scaling down', async () => { + const description = await pinecone.describeIndex(indexName); + expect(description.database?.replicas).toEqual(2); - // Scale down await pinecone.configureIndex(indexName, { replicas: 1 }); const description3 = await pinecone.describeIndex(indexName); expect(description3.database?.replicas).toEqual(1); From 05bce0f0dda46060c57bd0c6ba85969188c8816a Mon Sep 17 00:00:00 2001 From: Jen Hamon Date: Mon, 9 Oct 2023 09:27:02 -0400 Subject: [PATCH 6/7] Adjust describeIndex test --- .github/workflows/pr.yml | 1 - src/errors/http.ts | 12 +++--------- src/integration/control/describeIndex.test.ts | 7 ++----- 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index b15549d1..2dac6c26 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -41,7 +41,6 @@ jobs: name: Run integration tests runs-on: ubuntu-latest strategy: - max-parallel: 1 fail-fast: false matrix: jest_env: ['node', 'edge'] diff --git a/src/errors/http.ts b/src/errors/http.ts index 4c8ed71b..8f48a431 100644 --- a/src/errors/http.ts +++ b/src/errors/http.ts @@ -53,13 +53,11 @@ export class PineconeAuthorizationError extends BasePineconeError { */ export class PineconeNotFoundError extends BasePineconeError { constructor(failedRequest: FailedRequestInfo) { - const { url, message } = failedRequest; + const { url } = failedRequest; if (url) { super(`A call to ${url} returned HTTP status 404.`); - } else if (message) { - super(message); } else { - super(); + super('The requested resource could not be found.'); } this.name = 'PineconeNotFoundError'; @@ -77,10 +75,8 @@ export class PineconeConflictError extends BasePineconeError { super( `A call to ${url} returned HTTP status 409. ${message ? message : ''}` ); - } else if (message) { - super(message); } else { - super(); + super('The resource you are attempting to create already exists.'); } this.name = 'PineconeConflictError'; @@ -122,8 +118,6 @@ export class PineconeNotImplementedError extends BasePineconeError { super( `A call to ${url} returned HTTP status 501. ${message ? message : ''}` ); - } else if (message) { - super(message); } else { super(); } diff --git a/src/integration/control/describeIndex.test.ts b/src/integration/control/describeIndex.test.ts index 71144b3d..b799610f 100644 --- a/src/integration/control/describeIndex.test.ts +++ b/src/integration/control/describeIndex.test.ts @@ -1,4 +1,4 @@ -import { PineconeNotFoundError } from '../../errors'; +import { BasePineconeError } from '../../errors'; import { Pinecone } from '../../index'; import { randomIndexName } from '../test-helpers'; @@ -35,11 +35,8 @@ describe('describe index', () => { try { await pinecone.describeIndex('non-existent-index'); } catch (e) { - const err = e as PineconeNotFoundError; + const err = e as BasePineconeError; expect(err.name).toEqual('PineconeNotFoundError'); - expect(err.message).toEqual( - `A call to https://controller.${process.env.PINECONE_ENVIRONMENT}.pinecone.io/databases/non-existent-index returned HTTP status 404.` - ); } }); }); From ff6021722c14be049f01446c4e387144f381c0c2 Mon Sep 17 00:00:00 2001 From: Jen Hamon Date: Mon, 9 Oct 2023 13:13:00 -0400 Subject: [PATCH 7/7] Review feedback --- src/integration/errorHandling.test.ts | 2 +- src/utils/testHelper.ts | 11 ----------- 2 files changed, 1 insertion(+), 12 deletions(-) delete mode 100644 src/utils/testHelper.ts diff --git a/src/integration/errorHandling.test.ts b/src/integration/errorHandling.test.ts index a2a31b3a..e7f8f3a4 100644 --- a/src/integration/errorHandling.test.ts +++ b/src/integration/errorHandling.test.ts @@ -23,7 +23,7 @@ describe('Error handling', () => { test('calling data plane', async () => { const p = new Pinecone({ - apiKey: 'api-key-2', + apiKey: process.env.PINECONE_API_KEY || '', environment: 'wrong-environment2', }); diff --git a/src/utils/testHelper.ts b/src/utils/testHelper.ts deleted file mode 100644 index 98b147e3..00000000 --- a/src/utils/testHelper.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ResponseError } from '../pinecone-generated-ts-fetch'; - -export const responseError = (status: number, message: string) => { - return new ResponseError( - { - status, - text: async () => message, - } as Response, - 'oops' - ); -};