|
| 1 | +import { IcebergRestCatalog } from 'iceberg-js' |
1 | 2 | import { DEFAULT_HEADERS } from '../lib/constants' |
2 | 3 | import { isStorageError, StorageError } from '../lib/errors' |
3 | 4 | import { Fetch, get, post, remove } from '../lib/fetch' |
4 | | -import { resolveFetch } from '../lib/helpers' |
| 5 | +import { isValidBucketName, resolveFetch } from '../lib/helpers' |
5 | 6 | import { AnalyticBucket } from '../lib/types' |
6 | 7 |
|
7 | 8 | /** |
@@ -261,4 +262,163 @@ export default class StorageAnalyticsClient { |
261 | 262 | throw error |
262 | 263 | } |
263 | 264 | } |
| 265 | + |
| 266 | + /** |
| 267 | + * @alpha |
| 268 | + * |
| 269 | + * Get an Iceberg REST Catalog client configured for a specific analytics bucket |
| 270 | + * Use this to perform advanced table and namespace operations within the bucket |
| 271 | + * The returned client provides full access to the Apache Iceberg REST Catalog API |
| 272 | + * |
| 273 | + * **Public alpha:** This API is part of a public alpha release and may not be available to your account type. |
| 274 | + * |
| 275 | + * @category Analytics Buckets |
| 276 | + * @param bucketName - The name of the analytics bucket (warehouse) to connect to |
| 277 | + * @returns Configured IcebergRestCatalog instance for advanced Iceberg operations |
| 278 | + * |
| 279 | + * @example Get catalog and create table |
| 280 | + * ```js |
| 281 | + * // First, create an analytics bucket |
| 282 | + * const { data: bucket, error: bucketError } = await supabase |
| 283 | + * .storage |
| 284 | + * .analytics |
| 285 | + * .createBucket('analytics-data') |
| 286 | + * |
| 287 | + * // Get the Iceberg catalog for that bucket |
| 288 | + * const catalog = supabase.storage.analytics.from('analytics-data') |
| 289 | + * |
| 290 | + * // Create a namespace |
| 291 | + * await catalog.createNamespace({ namespace: ['default'] }) |
| 292 | + * |
| 293 | + * // Create a table with schema |
| 294 | + * await catalog.createTable( |
| 295 | + * { namespace: ['default'] }, |
| 296 | + * { |
| 297 | + * name: 'events', |
| 298 | + * schema: { |
| 299 | + * type: 'struct', |
| 300 | + * fields: [ |
| 301 | + * { id: 1, name: 'id', type: 'long', required: true }, |
| 302 | + * { id: 2, name: 'timestamp', type: 'timestamp', required: true }, |
| 303 | + * { id: 3, name: 'user_id', type: 'string', required: false } |
| 304 | + * ], |
| 305 | + * 'schema-id': 0, |
| 306 | + * 'identifier-field-ids': [1] |
| 307 | + * }, |
| 308 | + * 'partition-spec': { |
| 309 | + * 'spec-id': 0, |
| 310 | + * fields: [] |
| 311 | + * }, |
| 312 | + * 'write-order': { |
| 313 | + * 'order-id': 0, |
| 314 | + * fields: [] |
| 315 | + * }, |
| 316 | + * properties: { |
| 317 | + * 'write.format.default': 'parquet' |
| 318 | + * } |
| 319 | + * } |
| 320 | + * ) |
| 321 | + * ``` |
| 322 | + * |
| 323 | + * @example List tables in namespace |
| 324 | + * ```js |
| 325 | + * const catalog = supabase.storage.analytics.from('analytics-data') |
| 326 | + * |
| 327 | + * // List all tables in the default namespace |
| 328 | + * const tables = await catalog.listTables({ namespace: ['default'] }) |
| 329 | + * console.log(tables) // [{ namespace: ['default'], name: 'events' }] |
| 330 | + * ``` |
| 331 | + * |
| 332 | + * @example Working with namespaces |
| 333 | + * ```js |
| 334 | + * const catalog = supabase.storage.analytics.from('analytics-data') |
| 335 | + * |
| 336 | + * // List all namespaces |
| 337 | + * const namespaces = await catalog.listNamespaces() |
| 338 | + * |
| 339 | + * // Create namespace with properties |
| 340 | + * await catalog.createNamespace( |
| 341 | + * { namespace: ['production'] }, |
| 342 | + * { properties: { owner: 'data-team', env: 'prod' } } |
| 343 | + * ) |
| 344 | + * ``` |
| 345 | + * |
| 346 | + * @example Cleanup operations |
| 347 | + * ```js |
| 348 | + * const catalog = supabase.storage.analytics.from('analytics-data') |
| 349 | + * |
| 350 | + * // Drop table with purge option (removes all data) |
| 351 | + * await catalog.dropTable( |
| 352 | + * { namespace: ['default'], name: 'events' }, |
| 353 | + * { purge: true } |
| 354 | + * ) |
| 355 | + * |
| 356 | + * // Drop namespace (must be empty) |
| 357 | + * await catalog.dropNamespace({ namespace: ['default'] }) |
| 358 | + * ``` |
| 359 | + * |
| 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 | + * |
| 383 | + * @remarks |
| 384 | + * This method provides a bridge between Supabase's bucket management and the standard |
| 385 | + * Apache Iceberg REST Catalog API. The bucket name maps to the Iceberg warehouse parameter. |
| 386 | + * All authentication and configuration is handled automatically using your Supabase credentials. |
| 387 | + * |
| 388 | + * **Error Handling**: Operations may throw `IcebergError` from the iceberg-js library. |
| 389 | + * Always handle 404 errors gracefully when checking for resource existence. |
| 390 | + * |
| 391 | + * **Cleanup Operations**: When using `dropTable`, the `purge: true` option permanently |
| 392 | + * deletes all table data. Without it, the table is marked as deleted but data remains. |
| 393 | + * |
| 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 |
| 396 | + * [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. |
| 400 | + */ |
| 401 | + from(bucketName: string): IcebergRestCatalog { |
| 402 | + // Validate bucket name using same rules as Supabase Storage API backend |
| 403 | + if (!isValidBucketName(bucketName)) { |
| 404 | + throw new StorageError( |
| 405 | + 'Invalid bucket name: File, folder, and bucket names must follow AWS object key naming guidelines ' + |
| 406 | + 'and should avoid the use of any other characters.' |
| 407 | + ) |
| 408 | + } |
| 409 | + |
| 410 | + // Construct the Iceberg REST Catalog URL |
| 411 | + // The base URL is /storage/v1/iceberg |
| 412 | + // Note: IcebergRestCatalog from iceberg-js automatically adds /v1/ prefix to API paths |
| 413 | + // so we should NOT append /v1 here (it would cause double /v1/v1/ in the URL) |
| 414 | + return new IcebergRestCatalog({ |
| 415 | + baseUrl: this.url, |
| 416 | + catalogName: bucketName, // Maps to the warehouse parameter in Supabase's implementation |
| 417 | + auth: { |
| 418 | + type: 'custom', |
| 419 | + getHeaders: async () => this.headers, |
| 420 | + }, |
| 421 | + fetch: this.fetch, |
| 422 | + }) |
| 423 | + } |
264 | 424 | } |
0 commit comments