Files
supabase/apps/studio/data/replication/validate-destination-mutation.ts
T
Coenen BenjaminandJoshen Lim 2ce01473bb feat(replication): add better form to create ducklake replication (#47069)
## What kind of change does this PR introduce?

Add support to use supabase projects as a pg catalog and storage when
adding a ducklake replication.

## What is the current behavior?

Only simple form with raw input text for custom parameters is available.

## What is the new behavior?

Being able to select supabase project to directly use projects in
supabase for the ducklake.
I also fixed a warning we had in the console for this form (cf
screenshot)

## Additional context

[API Changes ](https://github.com/supabase/platform/pull/34282)


https://github.com/user-attachments/assets/4ff9ee65-6ba4-4f17-9ea1-9aebad34171c

<img width="862" height="228" alt="Capture d’écran 2026-06-18 à 09 58
50"
src="https://github.com/user-attachments/assets/1592c3be-807e-426f-9a5a-84979e05d93c"
/>

### Test scenario
Follow the screencast, go to your supabase project (better if it's in
ap-southeast-1)
Create a test table with 1 row for example
-> Database -> Replication -> New destination -> Select ducklake and use
supabase option
-> Keep the same current supabase project selected for both catalog and
storage
-> Create destination -> You'll get a warning about the storage and
credentials
-> Confirm creation
-> Wait until it's in status Running, if it's runing then it works


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

## Release Notes

* **New Features**
* Added DuckLake replication destination with **Use Supabase** and
**Custom parameters** modes.
* Added DuckLake bucket selection with a **“New bucket”** creation
dialog.
* Added/expanded BigQuery, Analytics Bucket, and Snowflake destination
configuration.

* **Improvements**
* Updated DuckLake create vs edit behavior: mode selection is hidden in
edit mode and configuration is mapped correctly for the selected
variant.
* Enhanced field-level validation (including whitespace-only handling)
and added clearer validation issue messages.
* Added a cross-region warning for DuckLake when catalog and storage
regions differ.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Benjamin <5719034+bnjjj@users.noreply.github.com>
Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2026-06-19 16:07:20 +02:00

141 lines
4.3 KiB
TypeScript

import { useMutation } from '@tanstack/react-query'
import type { components } from 'api-types'
import { buildDucklakeApiConfig, DestinationConfig } from './create-destination-pipeline-mutation'
import { handleError, post } from '@/data/fetchers'
import type { ResponseError, UseCustomMutationOptions } from '@/types'
type ValidateDestinationParams = {
projectRef: string
destinationConfig: DestinationConfig
sourceId?: number
publicationName?: string
maxFillMs?: number
maxTableSyncWorkers?: number
maxCopyConnectionsPerTable?: number
invalidatedSlotBehavior?: 'error' | 'recreate'
}
type ValidateDestinationResponse = components['schemas']['ValidateDestinationResponse']
export type ValidationFailure = ValidateDestinationResponse['validation_failures'][number]
async function validateDestination(
{
projectRef,
destinationConfig,
sourceId,
publicationName,
maxFillMs,
maxTableSyncWorkers,
maxCopyConnectionsPerTable,
invalidatedSlotBehavior,
}: ValidateDestinationParams,
signal?: AbortSignal
): Promise<ValidateDestinationResponse> {
if (!projectRef) throw new Error('projectRef is required')
// Build destination_config based on the type
let config: components['schemas']['ValidateReplicationDestinationBody']['config']
if ('bigQuery' in destinationConfig) {
const { projectId, datasetId, serviceAccountKey, connectionPoolSize, maxStalenessMins } =
destinationConfig.bigQuery
config = {
big_query: {
project_id: projectId,
dataset_id: datasetId,
service_account_key: serviceAccountKey,
connection_pool_size: connectionPoolSize,
max_staleness_mins: maxStalenessMins,
},
} as components['schemas']['ValidateReplicationDestinationBody']['config']
} else if ('iceberg' in destinationConfig) {
const {
projectRef: icebergProjectRef,
namespace,
warehouseName,
catalogToken,
s3AccessKeyId,
s3SecretAccessKey,
s3Region,
} = destinationConfig.iceberg
config = {
iceberg: {
supabase: {
namespace,
project_ref: icebergProjectRef,
warehouse_name: warehouseName,
catalog_token: catalogToken,
s3_access_key_id: s3AccessKeyId,
s3_secret_access_key: s3SecretAccessKey,
s3_region: s3Region,
},
},
}
} else if ('ducklake' in destinationConfig) {
config = buildDucklakeApiConfig(
destinationConfig.ducklake
) as components['schemas']['ValidateReplicationDestinationBody']['config']
} else if ('snowflake' in destinationConfig) {
const { accountId, user, privateKey, privateKeyPassphrase, database, schema, role } =
destinationConfig.snowflake
config = {
snowflake: {
account_id: accountId,
user,
private_key: privateKey,
private_key_passphrase: privateKeyPassphrase,
database,
schema,
role,
},
} as unknown as components['schemas']['ValidateReplicationDestinationBody']['config']
} else {
throw new Error(
'Invalid destination config: must specify bigQuery, iceberg, ducklake, or snowflake'
)
}
const batchConfig = maxFillMs !== undefined ? { max_fill_ms: maxFillMs } : undefined
const pipelineConfig =
publicationName === undefined
? undefined
: {
publication_name: publicationName,
max_table_sync_workers: maxTableSyncWorkers,
max_copy_connections_per_table: maxCopyConnectionsPerTable,
invalidated_slot_behavior: invalidatedSlotBehavior,
batch: batchConfig,
}
const { data, error } = await post('/platform/replication/{ref}/destinations/validate', {
params: { path: { ref: projectRef } },
body: {
config,
source_id: sourceId,
pipeline_config: pipelineConfig,
},
signal,
})
if (error) handleError(error)
return data as ValidateDestinationResponse
}
type ValidateDestinationData = Awaited<ReturnType<typeof validateDestination>>
export const useValidateDestinationMutation = (
options?: Omit<
UseCustomMutationOptions<ValidateDestinationData, ResponseError, ValidateDestinationParams>,
'mutationFn'
>
) => {
return useMutation<ValidateDestinationData, ResponseError, ValidateDestinationParams>({
mutationFn: (vars) => validateDestination(vars),
...options,
})
}