Files
supabase/apps/studio/components/interfaces/ConnectSheet/WarehouseModePanel/WarehouseModePanel.utils.ts
T
Coenen Benjamin 67e48f51b6 fix(warehouse): don't display ducklake metadata schema in replicated tables/schemas (#50123)
Don't show to users replicated schema or tables from the ducklake
metadata schema to avoid infinite loop. (This is already forbidden at
API level)


Signed-off-by: Benjamin <5719034+bnjjj@users.noreply.github.com>
2026-09-08 15:16:56 +02:00

114 lines
4.2 KiB
TypeScript

import type { components } from 'api-types'
import { INTERNAL_SCHEMAS } from '@/hooks/useProtectedSchemas'
import { WAREHOUSE_METADATA_SCHEMA } from '@/lib/warehouse'
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.
*
* `WAREHOUSE_METADATA_SCHEMA` is excluded on top of the infrastructure schemas above: it holds the
* DuckLake catalog describing the Warehouse itself, so replicating it would feed every Warehouse
* write back in as more catalog rows to replicate. The platform rejects it server-side too — this
* just keeps it out of the picker so the user never picks a target that can only fail.
*/
export function isSelectableWarehouseSchema(schemaName: string): boolean {
return (
!schemaName.startsWith('pg_') &&
!NON_SELECTABLE_SCHEMAS.has(schemaName) &&
schemaName !== WAREHOUSE_METADATA_SCHEMA
)
}
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']
>