Files
supabase/apps/studio/data/replication/validate-destination-mutation.ts
Victor Farazdagi 61754a0eec feat(studio): add Snowflake replication destination (#46767)
## Details of change

Adds Snowflake to the Studio replication destination flow:
- destination selection and display
- create/edit form fields
- validate/create/update payload serialization
- generated Platform API types

Snowflake remains gated behind `etlEnableSnowflakePrivateAlpha`.

**Note:** I have configured `etlEnableSnowflakePrivateAlpha` in
ConfigCat ("all" in staging and tied to my own org id in prod).

## Details of Verification Process

- Studio focused Vitest coverage for form serialization and diagram
mapping
- Studio typecheck
- ESLint on changed Studio replication files
- Local `mise fullstack:dev` smoke test to confirm the Snowflake form
renders ok.

<img width="937" height="569" alt="image"
src="https://github.com/user-attachments/assets/8d6b3a87-1f9d-4a59-91da-be719714ea49"
/>


Full create/validate E2E depends on the Platform PR and ETL runtime
rollout.


## Review Requests

Please check the Snowflake wire payload matches the Platform/ETL
contract and that gating/edit/display behavior follows the existing ETL
destination patterns.

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

* **New Features**
* Snowflake added as a supported replication destination (private-alpha
gated), including UI for selecting and configuring connection and auth
(account, user, database, schema, role, private key, optional
passphrase).
* **Validation**
* Form validation and submission now handle Snowflake-specific
required/optional fields.
* **Tests**
* Unit tests added for Snowflake form behavior and replication-type
detection.
* **API**
* Destination create/update/validate flows extended to accept Snowflake
payloads.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-11 10:25:57 +03:00

134 lines
4.0 KiB
TypeScript

import { useMutation } from '@tanstack/react-query'
import type { components } from 'api-types'
import { DestinationConfig } from './create-destination-pipeline-mutation'
import { handleError, post } from '@/data/fetchers'
import type { ResponseError, UseCustomMutationOptions } from '@/types'
type ValidateDestinationParams = {
projectRef: string
destinationConfig: DestinationConfig
}
type ValidateDestinationResponse = components['schemas']['ValidateDestinationResponse']
export type ValidationFailure = ValidateDestinationResponse['validation_failures'][number]
async function validateDestination(
{ projectRef, destinationConfig }: 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) {
const {
catalogUrl,
dataPath,
poolSize,
s3AccessKeyId,
s3SecretAccessKey,
s3Region,
s3Endpoint,
s3UrlStyle,
s3UseSsl,
metadataSchema,
} = destinationConfig.ducklake
config = {
ducklake: {
catalog_url: catalogUrl,
data_path: dataPath,
pool_size: poolSize,
s3_access_key_id: s3AccessKeyId,
s3_secret_access_key: s3SecretAccessKey,
s3_region: s3Region,
s3_endpoint: s3Endpoint,
s3_url_style: s3UrlStyle,
s3_use_ssl: s3UseSsl,
metadata_schema: metadataSchema,
},
} as unknown 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 { data, error } = await post('/platform/replication/{ref}/destinations/validate', {
params: { path: { ref: projectRef } },
body: { config },
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,
})
}