Skip to content

Commit a667ec5

Browse files
committed
fix(storage): add custom endpoints for analytics buckets
1 parent 35ace58 commit a667ec5

File tree

4 files changed

+237
-1
lines changed

4 files changed

+237
-1
lines changed

packages/core/storage-js/src/StorageClient.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import StorageFileApi from './packages/StorageFileApi'
22
import StorageBucketApi from './packages/StorageBucketApi'
3+
import StorageAnalyticsApi from './packages/StorageAnalyticsApi'
34
import { Fetch } from './lib/fetch'
45
import { StorageVectorsClient } from '@supabase/storage-vectors-js'
56

@@ -31,10 +32,33 @@ export class StorageClient extends StorageBucketApi {
3132
*
3233
* @returns A StorageVectorsClient instance configured with the current storage settings.
3334
*/
34-
vectors(): StorageVectorsClient {
35+
get vectors(): StorageVectorsClient {
3536
return new StorageVectorsClient(this.url + '/vector', {
3637
headers: this.headers,
3738
fetch: this.fetch,
3839
})
3940
}
41+
42+
/**
43+
* Access analytics storage operations using Iceberg tables.
44+
*
45+
* @returns A StorageAnalyticsApi instance configured with the current storage settings.
46+
* @example
47+
* ```typescript
48+
* const client = createClient(url, key)
49+
* const analytics = client.storage.analytics
50+
*
51+
* // Create an analytics bucket
52+
* await analytics.createBucket('my-analytics-bucket', { public: false })
53+
*
54+
* // List all analytics buckets
55+
* const { data: buckets } = await analytics.listBuckets()
56+
*
57+
* // Delete an analytics bucket
58+
* await analytics.deleteBucket('old-analytics-bucket')
59+
* ```
60+
*/
61+
get analytics(): StorageAnalyticsApi {
62+
return new StorageAnalyticsApi(this.url + '/iceberg', this.headers, this.fetch)
63+
}
4064
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export { StorageClient } from './StorageClient'
22
export type { StorageClientOptions } from './StorageClient'
3+
export { default as StorageAnalyticsApi } from './packages/StorageAnalyticsApi'
34
export * from './lib/types'
45
export * from './lib/errors'
56
export * from '@supabase/storage-vectors-js'

packages/core/storage-js/src/lib/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ export interface Bucket {
1414
public: boolean
1515
}
1616

17+
export interface AnalyticBucket {
18+
id: string
19+
type: "ANALYTICS"
20+
format: string
21+
created_at: string
22+
updated_at: string
23+
}
24+
1725
export interface FileObject {
1826
name: string
1927
bucket_id: string
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import { DEFAULT_HEADERS } from '../lib/constants'
2+
import { isStorageError, StorageError } from '../lib/errors'
3+
import { Fetch, get, post, remove } from '../lib/fetch'
4+
import { resolveFetch } from '../lib/helpers'
5+
import { AnalyticBucket, Bucket } from '../lib/types'
6+
7+
/**
8+
* API class for managing Analytics Buckets using Iceberg tables
9+
* Provides methods for creating, listing, and deleting analytics buckets
10+
*/
11+
export default class StorageAnalyticsApi {
12+
protected url: string
13+
protected headers: { [key: string]: string }
14+
protected fetch: Fetch
15+
protected shouldThrowOnError = false
16+
17+
constructor(url: string, headers: { [key: string]: string } = {}, fetch?: Fetch) {
18+
this.url = url.replace(/\/$/, '')
19+
this.headers = { ...DEFAULT_HEADERS, ...headers }
20+
this.fetch = resolveFetch(fetch)
21+
}
22+
23+
/**
24+
* Enable throwing errors instead of returning them in the response
25+
* When enabled, failed operations will throw instead of returning { data: null, error }
26+
*
27+
* @returns This instance for method chaining
28+
*/
29+
public throwOnError(): this {
30+
this.shouldThrowOnError = true
31+
return this
32+
}
33+
34+
/**
35+
* Creates a new analytics bucket using Iceberg tables
36+
* Analytics buckets are optimized for analytical queries and data processing
37+
*
38+
* @param name A unique name for the bucket you are creating
39+
* @returns Promise with newly created bucket name or error
40+
*
41+
* @example
42+
* ```typescript
43+
* const { data, error } = await storage.analytics.createBucket('analytics-data')
44+
* if (error) {
45+
* console.error('Failed to create analytics bucket:', error.message)
46+
* } else {
47+
* console.log('Created bucket:', data.name)
48+
* }
49+
* ```
50+
*/
51+
async createBucket(
52+
name: string
53+
): Promise<
54+
| {
55+
data: AnalyticBucket,
56+
error: null
57+
}
58+
| {
59+
data: null
60+
error: StorageError
61+
}
62+
> {
63+
try {
64+
const data = await post(
65+
this.fetch,
66+
`${this.url}/bucket`,
67+
{ name },
68+
{ headers: this.headers }
69+
)
70+
return { data, error: null }
71+
} catch (error) {
72+
if (this.shouldThrowOnError) {
73+
throw error
74+
}
75+
if (isStorageError(error)) {
76+
return { data: null, error }
77+
}
78+
79+
throw error
80+
}
81+
}
82+
83+
/**
84+
* Retrieves the details of all Analytics Storage buckets within an existing project
85+
* Only returns buckets of type 'ANALYTICS'
86+
*
87+
* @param options Query parameters for listing buckets
88+
* @param options.limit Maximum number of buckets to return
89+
* @param options.offset Number of buckets to skip
90+
* @param options.sortColumn Column to sort by ('id', 'name', 'created_at', 'updated_at')
91+
* @param options.sortOrder Sort order ('asc' or 'desc')
92+
* @param options.search Search term to filter bucket names
93+
* @returns Promise with list of analytics buckets or error
94+
*
95+
* @example
96+
* ```typescript
97+
* const { data, error } = await storage.analytics.listBuckets({
98+
* limit: 10,
99+
* offset: 0,
100+
* sortColumn: 'created_at',
101+
* sortOrder: 'desc',
102+
* search: 'analytics'
103+
* })
104+
* if (data) {
105+
* console.log('Found analytics buckets:', data.length)
106+
* data.forEach(bucket => console.log(`- ${bucket.name}`))
107+
* }
108+
* ```
109+
*/
110+
async listBuckets(options?: {
111+
limit?: number
112+
offset?: number
113+
sortColumn?: 'id' | 'name' | 'created_at' | 'updated_at'
114+
sortOrder?: 'asc' | 'desc'
115+
search?: string
116+
}): Promise<
117+
| {
118+
data: AnalyticBucket[]
119+
error: null
120+
}
121+
| {
122+
data: null
123+
error: StorageError
124+
}
125+
> {
126+
try {
127+
// Build query string from options
128+
const queryParams = new URLSearchParams()
129+
if (options?.limit !== undefined) queryParams.set('limit', options.limit.toString())
130+
if (options?.offset !== undefined) queryParams.set('offset', options.offset.toString())
131+
if (options?.sortColumn) queryParams.set('sortColumn', options.sortColumn)
132+
if (options?.sortOrder) queryParams.set('sortOrder', options.sortOrder)
133+
if (options?.search) queryParams.set('search', options.search)
134+
135+
const queryString = queryParams.toString()
136+
const url = queryString ? `${this.url}/bucket?${queryString}` : `${this.url}/bucket`
137+
138+
const data = await get(this.fetch, url, { headers: this.headers })
139+
// Filter to only return analytics buckets
140+
const analyticsBuckets = Array.isArray(data)
141+
? data.filter((bucket: Bucket) => bucket.type === 'ANALYTICS')
142+
: []
143+
return { data: analyticsBuckets, error: null }
144+
} catch (error) {
145+
if (this.shouldThrowOnError) {
146+
throw error
147+
}
148+
if (isStorageError(error)) {
149+
return { data: null, error }
150+
}
151+
152+
throw error
153+
}
154+
}
155+
156+
/**
157+
* Deletes an existing analytics bucket
158+
* A bucket can't be deleted with existing objects inside it
159+
* You must first empty the bucket before deletion
160+
*
161+
* @param bucketId The unique identifier of the bucket you would like to delete
162+
* @returns Promise with success message or error
163+
*
164+
* @example
165+
* ```typescript
166+
* const { data, error } = await analyticsApi.deleteBucket('old-analytics-bucket')
167+
* if (error) {
168+
* console.error('Failed to delete bucket:', error.message)
169+
* } else {
170+
* console.log('Bucket deleted successfully:', data.message)
171+
* }
172+
* ```
173+
*/
174+
async deleteBucket(bucketId: string): Promise<
175+
| {
176+
data: { message: string }
177+
error: null
178+
}
179+
| {
180+
data: null
181+
error: StorageError
182+
}
183+
> {
184+
try {
185+
const data = await remove(
186+
this.fetch,
187+
`${this.url}/bucket/${bucketId}`,
188+
{},
189+
{ headers: this.headers }
190+
)
191+
return { data, error: null }
192+
} catch (error) {
193+
if (this.shouldThrowOnError) {
194+
throw error
195+
}
196+
if (isStorageError(error)) {
197+
return { data: null, error }
198+
}
199+
200+
throw error
201+
}
202+
}
203+
}

0 commit comments

Comments
 (0)