1- import { IcebergRestCatalog } from 'iceberg-js'
1+ import { IcebergRestCatalog , IcebergError } from 'iceberg-js'
22import { DEFAULT_HEADERS } from '../lib/constants'
33import { isStorageError , StorageError } from '../lib/errors'
44import { Fetch , get , post , remove } from '../lib/fetch'
55import { isValidBucketName , resolveFetch } from '../lib/helpers'
66import { AnalyticBucket } from '../lib/types'
77
8+ type WrapAsyncMethod < T > = T extends ( ...args : infer A ) => Promise < infer R >
9+ ? ( ...args : A ) => Promise < { data : R ; error : null } | { data : null ; error : IcebergError } >
10+ : T
11+
12+ export type WrappedIcebergRestCatalog = {
13+ [ K in keyof IcebergRestCatalog ] : WrapAsyncMethod < IcebergRestCatalog [ K ] >
14+ }
15+
816/**
917 * Client class for managing Analytics Buckets using Iceberg tables
1018 * Provides methods for creating, listing, and deleting analytics buckets
@@ -269,12 +277,14 @@ export default class StorageAnalyticsClient {
269277 * Get an Iceberg REST Catalog client configured for a specific analytics bucket
270278 * Use this to perform advanced table and namespace operations within the bucket
271279 * The returned client provides full access to the Apache Iceberg REST Catalog API
280+ * with the Supabase `{ data, error }` pattern for consistent error handling on all operations.
272281 *
273282 * **Public alpha:** This API is part of a public alpha release and may not be available to your account type.
274283 *
275284 * @category Analytics Buckets
276285 * @param bucketName - The name of the analytics bucket (warehouse) to connect to
277- * @returns Configured IcebergRestCatalog instance for advanced Iceberg operations
286+ * @returns The wrapped Iceberg catalog client
287+ * @throws {StorageError } If the bucket name is invalid
278288 *
279289 * @example Get catalog and create table
280290 * ```js
@@ -288,10 +298,10 @@ export default class StorageAnalyticsClient {
288298 * const catalog = supabase.storage.analytics.from('analytics-data')
289299 *
290300 * // Create a namespace
291- * await catalog.createNamespace({ namespace: ['default'] })
301+ * const { error: nsError } = await catalog.createNamespace({ namespace: ['default'] })
292302 *
293303 * // Create a table with schema
294- * await catalog.createTable(
304+ * const { data: tableMetadata, error: tableError } = await catalog.createTable(
295305 * { namespace: ['default'] },
296306 * {
297307 * name: 'events',
@@ -325,7 +335,13 @@ export default class StorageAnalyticsClient {
325335 * const catalog = supabase.storage.analytics.from('analytics-data')
326336 *
327337 * // List all tables in the default namespace
328- * const tables = await catalog.listTables({ namespace: ['default'] })
338+ * const { data: tables, error: listError } = await catalog.listTables({ namespace: ['default'] })
339+ * if (listError) {
340+ * if (listError.isNotFound()) {
341+ * console.log('Namespace not found')
342+ * }
343+ * return
344+ * }
329345 * console.log(tables) // [{ namespace: ['default'], name: 'events' }]
330346 * ```
331347 *
@@ -334,7 +350,7 @@ export default class StorageAnalyticsClient {
334350 * const catalog = supabase.storage.analytics.from('analytics-data')
335351 *
336352 * // List all namespaces
337- * const namespaces = await catalog.listNamespaces()
353+ * const { data: namespaces } = await catalog.listNamespaces()
338354 *
339355 * // Create namespace with properties
340356 * await catalog.createNamespace(
@@ -348,57 +364,37 @@ export default class StorageAnalyticsClient {
348364 * const catalog = supabase.storage.analytics.from('analytics-data')
349365 *
350366 * // Drop table with purge option (removes all data)
351- * await catalog.dropTable(
367+ * const { error: dropError } = await catalog.dropTable(
352368 * { namespace: ['default'], name: 'events' },
353369 * { purge: true }
354370 * )
355371 *
372+ * if (dropError?.isNotFound()) {
373+ * console.log('Table does not exist')
374+ * }
375+ *
356376 * // Drop namespace (must be empty)
357377 * await catalog.dropNamespace({ namespace: ['default'] })
358378 * ```
359379 *
360- * @example Error handling with catalog operations
361- * ```js
362- * import { IcebergError } from 'iceberg-js'
363- *
364- * const catalog = supabase.storage.analytics.from('analytics-data')
365- *
366- * try {
367- * await catalog.dropTable({ namespace: ['default'], name: 'events' }, { purge: true })
368- * } catch (error) {
369- * // Handle 404 errors (resource not found)
370- * const is404 =
371- * (error instanceof IcebergError && error.status === 404) ||
372- * error?.status === 404 ||
373- * error?.details?.error?.code === 404
374- *
375- * if (is404) {
376- * console.log('Table does not exist')
377- * } else {
378- * throw error // Re-throw other errors
379- * }
380- * }
381- * ```
382- *
383380 * @remarks
384381 * This method provides a bridge between Supabase's bucket management and the standard
385382 * Apache Iceberg REST Catalog API. The bucket name maps to the Iceberg warehouse parameter.
386383 * All authentication and configuration is handled automatically using your Supabase credentials.
387384 *
388- * **Error Handling**: Operations may throw `IcebergError` from the iceberg-js library.
389- * Always handle 404 errors gracefully when checking for resource existence.
385+ * **Error Handling**: Invalid bucket names throw immediately. All catalog
386+ * operations return `{ data, error }` where errors are `IcebergError` instances from iceberg-js.
387+ * Use helper methods like `error.isNotFound()` or check `error.status` for specific error handling.
388+ * Use `.throwOnError()` on the analytics client if you prefer exceptions for catalog operations.
390389 *
391390 * **Cleanup Operations**: When using `dropTable`, the `purge: true` option permanently
392391 * deletes all table data. Without it, the table is marked as deleted but data remains.
393392 *
394- * **Library Dependency**: The returned catalog is an instance of `IcebergRestCatalog`
395- * from iceberg-js. For complete API documentation and advanced usage, refer to the
393+ * **Library Dependency**: The returned catalog wraps `IcebergRestCatalog` from iceberg-js.
394+ * For complete API documentation and advanced usage, refer to the
396395 * [iceberg-js documentation](https://supabase.github.io/iceberg-js/).
397- *
398- * For advanced Iceberg operations beyond bucket management, you can also install and use
399- * the `iceberg-js` package directly with manual configuration.
400396 */
401- from ( bucketName : string ) : IcebergRestCatalog {
397+ from ( bucketName : string ) : WrappedIcebergRestCatalog {
402398 // Validate bucket name using same rules as Supabase Storage API backend
403399 if ( ! isValidBucketName ( bucketName ) ) {
404400 throw new StorageError (
@@ -411,7 +407,7 @@ export default class StorageAnalyticsClient {
411407 // The base URL is /storage/v1/iceberg
412408 // Note: IcebergRestCatalog from iceberg-js automatically adds /v1/ prefix to API paths
413409 // so we should NOT append /v1 here (it would cause double /v1/v1/ in the URL)
414- return new IcebergRestCatalog ( {
410+ const catalog = new IcebergRestCatalog ( {
415411 baseUrl : this . url ,
416412 catalogName : bucketName , // Maps to the warehouse parameter in Supabase's implementation
417413 auth : {
@@ -420,5 +416,30 @@ export default class StorageAnalyticsClient {
420416 } ,
421417 fetch : this . fetch ,
422418 } )
419+
420+ const shouldThrowOnError = this . shouldThrowOnError
421+
422+ const wrappedCatalog = new Proxy ( catalog , {
423+ get ( target , prop : keyof IcebergRestCatalog ) {
424+ const value = target [ prop ]
425+ if ( typeof value !== 'function' ) {
426+ return value
427+ }
428+
429+ return async ( ...args : unknown [ ] ) => {
430+ try {
431+ const data = await ( value as Function ) . apply ( target , args )
432+ return { data, error : null }
433+ } catch ( error ) {
434+ if ( shouldThrowOnError ) {
435+ throw error
436+ }
437+ return { data : null , error : error as IcebergError }
438+ }
439+ }
440+ } ,
441+ } ) as unknown as WrappedIcebergRestCatalog
442+
443+ return wrappedCatalog
423444 }
424445}
0 commit comments