Files
supabase/apps/studio/components/interfaces/ConnectSheet/WarehouseModePanel/WarehouseModePanel.utils.ts
Coenen BenjaminandCharis Lam 11289328e5 add support for warehouse connection string (#49914)
Add support for connection string for warehouse. 
This PR gives the ability to enable warehouse on a project and also get
the connection string to connect to.

> This project is only available in staging for now and gated behind a
feature flag

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
  - Added a Warehouse connection option to the Connect dialog.
- Select schemas and tables to replicate, with setup progress, error
recovery, and retry support.
- View copyable Warehouse connection details, credentials guidance,
command-line instructions, and DuckLake setup scripts.
  - Warehouse availability is controlled by feature configuration.

- **Tests**
- Added coverage for Warehouse table selection, setup script generation,
URL parsing, and connection configuration utilities.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Benjamin <5719034+bnjjj@users.noreply.github.com>
Co-authored-by: Charis Lam <26616127+charislam@users.noreply.github.com>
2026-09-07 11:24:42 +02:00

102 lines
3.7 KiB
TypeScript

import type { components } from 'api-types'
import { INTERNAL_SCHEMAS } from '@/hooks/useProtectedSchemas'
export type WarehouseSetupBody = components['schemas']['WarehouseSetupBody']
export type WarehouseSetupTarget = WarehouseSetupBody['targets'][number]
/** Selection map keyed by `${schema}.${table}`. */
export type SchemaTableSelection = Record<string, boolean>
export type SchemaWithTables = { schema: string; tables: string[] }
export function getSchemaTableKey(schema: string, table: string): string {
return `${schema}.${table}`
}
/**
* Internal schemas that still hold product data users legitimately want in their warehouse.
* Everything else in `INTERNAL_SCHEMAS` is Supabase infrastructure — `vault` (secrets),
* `pgsodium`, `cron`/`pgmq` bookkeeping, migration history — which should never be offered as a
* replication target.
*/
const REPLICABLE_INTERNAL_SCHEMAS = ['auth', 'storage']
const NON_SELECTABLE_SCHEMAS = new Set(
INTERNAL_SCHEMAS.filter((schema) => !REPLICABLE_INTERNAL_SCHEMAS.includes(schema))
)
/** Postgres schemas Warehouse setup shouldn't offer for replication. */
export function isSelectableWarehouseSchema(schemaName: string): boolean {
return !schemaName.startsWith('pg_') && !NON_SELECTABLE_SCHEMAS.has(schemaName)
}
export function getSelectedTableCount(selection: SchemaTableSelection): number {
return Object.values(selection).filter(Boolean).length
}
/**
* Seeds the picker's selection from the tables already in the `supabase_warehouse` publication, so
* editing an existing setup starts from what's actually replicated instead of an empty selection.
* Schema-level checkboxes derive from these per-table entries, so a schema whose every table is in
* the publication ends up fully checked on its own.
*/
export function buildSelectionFromPublicationTables(
publicationTables: { schema: string; name: string }[]
): SchemaTableSelection {
return publicationTables.reduce<SchemaTableSelection>((selection, table) => {
selection[getSchemaTableKey(table.schema, table.name)] = true
return selection
}, {})
}
/**
* Tri-state value for a schema's checkbox. Kept here (rather than inlined as nested ternaries in
* the picker) so the three cases stay explicit and testable.
*/
export function getSchemaCheckedState({
selectedCount,
totalCount,
}: {
selectedCount: number
totalCount: number
}): boolean | 'indeterminate' {
if (totalCount > 0 && selectedCount === totalCount) return true
if (selectedCount > 0) return 'indeterminate'
return false
}
/**
* Maps the schema/table checkbox selection down to the API's `targets` shape. A schema whose
* every currently-known table is selected is sent as a single `{ type: 'schema' }` target
* (matching the API's semantics of "the currently eligible tables in that schema"); otherwise each
* selected table is sent individually. Schemas with no tables, or no selected tables, are omitted.
*/
export function buildWarehouseSetupTargets(
selection: SchemaTableSelection,
schemasWithTables: SchemaWithTables[]
): WarehouseSetupTarget[] {
const targets: WarehouseSetupTarget[] = []
for (const { schema, tables } of schemasWithTables) {
if (tables.length === 0) continue
const selectedTables = tables.filter((table) => selection[getSchemaTableKey(schema, table)])
if (selectedTables.length === 0) continue
if (selectedTables.length === tables.length) {
targets.push({ type: 'schema', schema })
} else {
selectedTables.forEach((name) => {
targets.push({ type: 'table', schema, name })
})
}
}
return targets
}
export type WarehouseCatalogCredentials = NonNullable<
components['schemas']['WarehouseCatalogResponse']['credentials']
>