Skip to content

Commit 6a2ba02

Browse files
fenosmandarini
authored andcommitted
feat(storage): implement vector client
1 parent d2ac118 commit 6a2ba02

25 files changed

+5189
-44
lines changed

packages/integrations/storage-vectors-js/README.md

Lines changed: 397 additions & 6 deletions
Large diffs are not rendered by default.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/* eslint-disable */
2+
3+
module.exports = {
4+
displayName: 'storage-vectors-js',
5+
preset: '../../../jest.preset.js',
6+
testEnvironment: 'node',
7+
transform: {
8+
'^.+\\.[tj]s$': [
9+
'@swc/jest',
10+
{
11+
jsc: {
12+
target: 'es2017',
13+
parser: {
14+
syntax: 'typescript',
15+
decorators: true,
16+
dynamicImport: true,
17+
},
18+
transform: {
19+
decoratorMetadata: true,
20+
legacyDecorator: true,
21+
},
22+
keepClassNames: true,
23+
externalHelpers: true,
24+
loose: true,
25+
},
26+
module: {
27+
type: 'commonjs',
28+
},
29+
sourceMaps: true,
30+
exclude: [],
31+
},
32+
],
33+
},
34+
moduleFileExtensions: ['ts', 'js', 'html'],
35+
coverageDirectory: 'test-output/jest/coverage',
36+
testMatch: ['**/__tests__/**/*.spec.ts'],
37+
moduleNameMapper: {
38+
'^(\\.{1,2}/.*)\\.js$': '$1',
39+
},
40+
}

packages/integrations/storage-vectors-js/jest.config.ts

Lines changed: 0 additions & 19 deletions
This file was deleted.

packages/integrations/storage-vectors-js/package.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,21 @@
1818
"dist",
1919
"!**/*.tsbuildinfo"
2020
],
21+
"scripts": {
22+
"test": "jest",
23+
"test:watch": "jest --watch",
24+
"test:coverage": "jest --coverage",
25+
"test:real": "USE_MOCK_SERVER=false jest",
26+
"test:mock": "USE_MOCK_SERVER=true jest"
27+
},
2128
"nx": {
2229
"name": "@supabase/storage-vectors-js"
2330
},
2431
"dependencies": {
25-
"tslib": "^2.3.0"
32+
"@supabase/node-fetch": "^2.6.13"
33+
},
34+
"devDependencies": {
35+
"@types/node": "^24.7.2",
36+
"tslib": "^2.8.1"
2637
}
2738
}
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
/**
2+
* Integration tests for Vector Bucket API
3+
* Tests all bucket operations: create, get, list, delete
4+
*/
5+
6+
import { createTestClient, setupTest, generateTestName, assertSuccessResponse, assertErrorResponse, assertErrorCode } from './helpers'
7+
8+
describe('VectorBucketApi Integration Tests', () => {
9+
let client: ReturnType<typeof createTestClient>
10+
11+
beforeEach(() => {
12+
setupTest()
13+
client = createTestClient()
14+
})
15+
16+
describe('createVectorBucket', () => {
17+
it('should create a new vector bucket successfully', async () => {
18+
const bucketName = generateTestName('test-bucket')
19+
20+
const response = await client.createVectorBucket(bucketName)
21+
22+
assertSuccessResponse(response)
23+
expect(response.data).toEqual({})
24+
})
25+
26+
it('should return conflict error when bucket already exists', async () => {
27+
const bucketName = generateTestName('test-bucket')
28+
29+
// Create bucket first time
30+
await client.createVectorBucket(bucketName)
31+
32+
// Try to create again
33+
const response = await client.createVectorBucket(bucketName)
34+
35+
const error = assertErrorResponse(response)
36+
assertErrorCode(error, 'S3VectorConflictException')
37+
expect(error.message).toContain('already exists')
38+
})
39+
40+
it('should create multiple buckets with different names', async () => {
41+
const bucket1 = generateTestName('test-bucket-1')
42+
const bucket2 = generateTestName('test-bucket-2')
43+
44+
const response1 = await client.createVectorBucket(bucket1)
45+
const response2 = await client.createVectorBucket(bucket2)
46+
47+
assertSuccessResponse(response1)
48+
assertSuccessResponse(response2)
49+
})
50+
})
51+
52+
describe('getVectorBucket', () => {
53+
it('should retrieve an existing bucket', async () => {
54+
const bucketName = generateTestName('test-bucket')
55+
56+
// Create bucket
57+
await client.createVectorBucket(bucketName)
58+
59+
// Retrieve bucket
60+
const response = await client.getVectorBucket(bucketName)
61+
62+
const data = assertSuccessResponse(response)
63+
expect(data.vectorBucket).toBeDefined()
64+
expect(data.vectorBucket.vectorBucketName).toBe(bucketName)
65+
expect(data.vectorBucket.creationTime).toBeDefined()
66+
expect(typeof data.vectorBucket.creationTime).toBe('number')
67+
})
68+
69+
it('should return not found error for non-existent bucket', async () => {
70+
const response = await client.getVectorBucket('non-existent-bucket')
71+
72+
const error = assertErrorResponse(response)
73+
assertErrorCode(error, 'S3VectorNotFoundException')
74+
expect(error.message).toContain('not found')
75+
})
76+
77+
it('should return bucket with encryption configuration if set', async () => {
78+
const bucketName = generateTestName('test-bucket')
79+
80+
await client.createVectorBucket(bucketName)
81+
const response = await client.getVectorBucket(bucketName)
82+
83+
const data = assertSuccessResponse(response)
84+
// Encryption configuration is optional
85+
if (data.vectorBucket.encryptionConfiguration) {
86+
expect(data.vectorBucket.encryptionConfiguration).toHaveProperty('sseType')
87+
}
88+
})
89+
})
90+
91+
describe('listVectorBuckets', () => {
92+
it('should list all buckets', async () => {
93+
const bucket1 = generateTestName('test-bucket-1')
94+
const bucket2 = generateTestName('test-bucket-2')
95+
96+
await client.createVectorBucket(bucket1)
97+
await client.createVectorBucket(bucket2)
98+
99+
const response = await client.listVectorBuckets()
100+
101+
const data = assertSuccessResponse(response)
102+
expect(data.buckets).toBeDefined()
103+
expect(Array.isArray(data.buckets)).toBe(true)
104+
expect(data.buckets.length).toBeGreaterThanOrEqual(2)
105+
106+
const bucketNames = data.buckets.map(b => b.vectorBucketName)
107+
expect(bucketNames).toContain(bucket1)
108+
expect(bucketNames).toContain(bucket2)
109+
})
110+
111+
it('should filter buckets by prefix', async () => {
112+
const prefix = generateTestName('prefix-test')
113+
const bucket1 = `${prefix}-bucket-1`
114+
const bucket2 = `${prefix}-bucket-2`
115+
const bucket3 = generateTestName('other-bucket')
116+
117+
await client.createVectorBucket(bucket1)
118+
await client.createVectorBucket(bucket2)
119+
await client.createVectorBucket(bucket3)
120+
121+
const response = await client.listVectorBuckets({ prefix })
122+
123+
const data = assertSuccessResponse(response)
124+
expect(data.buckets.length).toBeGreaterThanOrEqual(2)
125+
126+
const bucketNames = data.buckets.map(b => b.vectorBucketName)
127+
expect(bucketNames).toContain(bucket1)
128+
expect(bucketNames).toContain(bucket2)
129+
// bucket3 should not be included as it doesn't match prefix
130+
const hasOtherBucket = bucketNames.some(name => name.includes('other-bucket'))
131+
if (hasOtherBucket) {
132+
// If other buckets exist, they should match the prefix
133+
expect(bucketNames.every(name => name.startsWith(prefix))).toBe(true)
134+
}
135+
})
136+
137+
it('should support pagination with maxResults', async () => {
138+
const response = await client.listVectorBuckets({ maxResults: 1 })
139+
140+
const data = assertSuccessResponse(response)
141+
expect(data.buckets.length).toBeLessThanOrEqual(1)
142+
143+
if (data.buckets.length === 1 && data.nextToken) {
144+
expect(data.nextToken).toBeDefined()
145+
expect(typeof data.nextToken).toBe('string')
146+
}
147+
})
148+
149+
it('should return empty array when no buckets match prefix', async () => {
150+
const response = await client.listVectorBuckets({
151+
prefix: 'non-existent-prefix-' + Date.now(),
152+
})
153+
154+
const data = assertSuccessResponse(response)
155+
expect(data.buckets).toEqual([])
156+
expect(data.nextToken).toBeUndefined()
157+
})
158+
})
159+
160+
describe('deleteVectorBucket', () => {
161+
it('should delete an empty bucket successfully', async () => {
162+
const bucketName = generateTestName('test-bucket')
163+
164+
await client.createVectorBucket(bucketName)
165+
166+
const response = await client.deleteVectorBucket(bucketName)
167+
168+
assertSuccessResponse(response)
169+
expect(response.data).toEqual({})
170+
171+
// Verify bucket is deleted
172+
const getResponse = await client.getVectorBucket(bucketName)
173+
assertErrorResponse(getResponse)
174+
})
175+
176+
it('should return not found error for non-existent bucket', async () => {
177+
const response = await client.deleteVectorBucket('non-existent-bucket')
178+
179+
const error = assertErrorResponse(response)
180+
assertErrorCode(error, 'S3VectorNotFoundException')
181+
})
182+
183+
it('should return error when bucket is not empty', async () => {
184+
const bucketName = generateTestName('test-bucket')
185+
186+
await client.createVectorBucket(bucketName)
187+
188+
// Create an index in the bucket
189+
const bucket = client.from(bucketName)
190+
await bucket.createIndex({
191+
indexName: 'test-index',
192+
dataType: 'float32',
193+
dimension: 3,
194+
distanceMetric: 'cosine',
195+
})
196+
197+
// Try to delete bucket with index
198+
const response = await client.deleteVectorBucket(bucketName)
199+
200+
const error = assertErrorResponse(response)
201+
assertErrorCode(error, 'S3VectorBucketNotEmpty')
202+
expect(error.message).toContain('not empty')
203+
})
204+
205+
it('should successfully delete bucket after removing all indexes', async () => {
206+
const bucketName = generateTestName('test-bucket')
207+
208+
await client.createVectorBucket(bucketName)
209+
210+
const bucket = client.from(bucketName)
211+
await bucket.createIndex({
212+
indexName: 'test-index',
213+
dataType: 'float32',
214+
dimension: 3,
215+
distanceMetric: 'cosine',
216+
})
217+
218+
// Delete the index first
219+
await bucket.deleteIndex('test-index')
220+
221+
// Now delete the bucket
222+
const response = await client.deleteVectorBucket(bucketName)
223+
224+
assertSuccessResponse(response)
225+
})
226+
})
227+
228+
describe('throwOnError mode', () => {
229+
it('should throw error instead of returning error response', async () => {
230+
client.throwOnError()
231+
232+
await expect(client.getVectorBucket('non-existent-bucket')).rejects.toThrow()
233+
})
234+
235+
it('should still return data on success', async () => {
236+
const bucketName = generateTestName('test-bucket')
237+
client.throwOnError()
238+
239+
await client.createVectorBucket(bucketName)
240+
const response = await client.getVectorBucket(bucketName)
241+
242+
expect(response.data).toBeDefined()
243+
expect(response.error).toBeNull()
244+
})
245+
})
246+
247+
describe('VectorBucketScope (from)', () => {
248+
it('should create a bucket scope successfully', async () => {
249+
const bucketName = generateTestName('test-bucket')
250+
251+
await client.createVectorBucket(bucketName)
252+
253+
const bucketScope = client.from(bucketName)
254+
255+
expect(bucketScope).toBeDefined()
256+
expect(typeof bucketScope.createIndex).toBe('function')
257+
expect(typeof bucketScope.listIndexes).toBe('function')
258+
expect(typeof bucketScope.index).toBe('function')
259+
})
260+
})
261+
})

0 commit comments

Comments
 (0)