mirror of
https://github.com/supabase/supabase.git
synced 2026-10-10 11:55:05 +03:00
* Fix childProps in Admonition so that they're added as a prop to the main div. * Replace the admonition with Alert in Wrapper tab page to add a gap between childs. * Add s3 vectors fdw. * Minor fix to FormSection. * Update the fdw mutations to support passing in options. * Refactor the vector flow to create fdws. * Revert cron description change. * If the bucket can't be created, don't create a fdw. * Update/delete the fdw when deleting a table or a bucket. * Minor fixes. * Clean up the delete modal. * Handle edge cases when missing a wrapper. * Remove the admonition in the create bucket modal. * Fix the loading state when creating a bucket. * Fix the createWrapper sheet to work with s3 vectors. * Fix undefined wrapperMeta issue. * Create the schema when installing a wrapper. * Tiny cleanup. * Clean up unneeded useState. Create a wrapper only if the all conditions are met. * Fix all comments. * Add s3 vectors for docs. * Add a link and fix the file name for S3 Vectors in docs. * Hide the table editor button if the wrapper instance is missing. * Small fixes.
277 lines
8.3 KiB
TypeScript
277 lines
8.3 KiB
TypeScript
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
import { toast } from 'sonner'
|
|
|
|
import {
|
|
AvailableColumn,
|
|
WrapperMeta,
|
|
} from 'components/interfaces/Integrations/Wrappers/Wrappers.types'
|
|
import { entityTypeKeys } from 'data/entity-types/keys'
|
|
import { foreignTableKeys } from 'data/foreign-tables/keys'
|
|
import { executeSql } from 'data/sql/execute-sql-query'
|
|
import { wrapWithTransaction } from 'data/sql/utils/transaction'
|
|
import { vaultSecretsKeys } from 'data/vault/keys'
|
|
import type { ResponseError, UseCustomMutationOptions } from 'types'
|
|
import { fdwKeys } from './keys'
|
|
|
|
export type FDWCreateVariables = {
|
|
projectRef?: string
|
|
connectionString?: string | null
|
|
wrapperMeta: WrapperMeta
|
|
formState: {
|
|
[k: string]: string
|
|
}
|
|
// If mode is skip, the wrapper will skip the last step, binding the schema/tables to foreign data. This could be done later.
|
|
mode: 'tables' | 'schema' | 'skip'
|
|
tables: any[]
|
|
sourceSchema: string
|
|
targetSchema: string
|
|
schemaOptions?: string[]
|
|
}
|
|
|
|
export function getCreateFDWSql({
|
|
wrapperMeta,
|
|
formState,
|
|
mode,
|
|
tables,
|
|
sourceSchema,
|
|
targetSchema,
|
|
schemaOptions = [],
|
|
}: Omit<FDWCreateVariables, 'projectRef' | 'connectionString'>) {
|
|
const newSchemasSql = tables
|
|
.filter((table) => table.is_new_schema)
|
|
.map((table) => /* SQL */ `create schema if not exists ${table.schema_name};`)
|
|
.join('\n')
|
|
|
|
const createWrapperSql = /* SQL */ `
|
|
create foreign data wrapper "${formState.wrapper_name}"
|
|
handler "${wrapperMeta.handlerName}"
|
|
validator "${wrapperMeta.validatorName}";
|
|
`
|
|
|
|
const encryptedOptions = wrapperMeta.server.options.filter((option) => option.encrypted)
|
|
const unencryptedOptions = wrapperMeta.server.options.filter((option) => !option.encrypted)
|
|
|
|
const createEncryptedKeysSqlArray = encryptedOptions.map((option) => {
|
|
const key = `${formState.wrapper_name}_${option.name}`
|
|
// Escape single quotes in postgresql by doubling them up
|
|
const value = (formState[option.name] || '').replace(/'/g, "''")
|
|
|
|
return /* SQL */ `
|
|
do $$
|
|
begin
|
|
-- Old wrappers has an implicit dependency on pgsodium. For new wrappers
|
|
-- we use Vault directly.
|
|
if (select extversion from pg_extension where extname = 'wrappers') in (
|
|
'0.1.0',
|
|
'0.1.1',
|
|
'0.1.4',
|
|
'0.1.5',
|
|
'0.1.6',
|
|
'0.1.7',
|
|
'0.1.8',
|
|
'0.1.9',
|
|
'0.1.10',
|
|
'0.1.11',
|
|
'0.1.12',
|
|
'0.1.14',
|
|
'0.1.15',
|
|
'0.1.16',
|
|
'0.1.17',
|
|
'0.1.18',
|
|
'0.1.19',
|
|
'0.2.0',
|
|
'0.3.0',
|
|
'0.3.1',
|
|
'0.4.0',
|
|
'0.4.1',
|
|
'0.4.2',
|
|
'0.4.3',
|
|
'0.4.4',
|
|
'0.4.5'
|
|
) then
|
|
create extension if not exists pgsodium;
|
|
|
|
perform pgsodium.create_key(
|
|
name := '${key}'
|
|
);
|
|
|
|
perform vault.create_secret(
|
|
new_secret := '${value}',
|
|
new_name := '${key}',
|
|
new_key_id := (select id from pgsodium.valid_key where name = '${key}')
|
|
);
|
|
else
|
|
perform vault.create_secret(
|
|
new_secret := '${value}',
|
|
new_name := '${key}'
|
|
);
|
|
end if;
|
|
end $$;
|
|
`
|
|
})
|
|
|
|
const createEncryptedKeysSql = createEncryptedKeysSqlArray.join('\n')
|
|
|
|
const encryptedOptionsSqlArray = encryptedOptions
|
|
.filter((option) => formState[option.name])
|
|
.map((option) => `${option.name} ''%s''`)
|
|
const unencryptedOptionsSqlArray = unencryptedOptions
|
|
.filter((option) => formState[option.name])
|
|
// wrap all option names in double quotes to handle dots
|
|
// wrap all options values in single quotes, replace single quotes with 4 single quotes to escape them in SQL past the execute format
|
|
.map((option) => `"${option.name}" ''${formState[option.name].replace(/'/g, `''''`)}''`)
|
|
const optionsSqlArray = [...encryptedOptionsSqlArray, ...unencryptedOptionsSqlArray].join(',')
|
|
|
|
const createServerSql = /* SQL */ `
|
|
do $$
|
|
declare
|
|
-- Old wrappers has an implicit dependency on pgsodium. For new wrappers
|
|
-- we use Vault directly.
|
|
is_using_old_wrappers bool;
|
|
${encryptedOptions.map((option) => `v_${option.name} text;`).join('\n')}
|
|
begin
|
|
is_using_old_wrappers := (select extversion from pg_extension where extname = 'wrappers') in (
|
|
'0.1.0',
|
|
'0.1.1',
|
|
'0.1.4',
|
|
'0.1.5',
|
|
'0.1.6',
|
|
'0.1.7',
|
|
'0.1.8',
|
|
'0.1.9',
|
|
'0.1.10',
|
|
'0.1.11',
|
|
'0.1.12',
|
|
'0.1.14',
|
|
'0.1.15',
|
|
'0.1.16',
|
|
'0.1.17',
|
|
'0.1.18',
|
|
'0.1.19',
|
|
'0.2.0',
|
|
'0.3.0',
|
|
'0.3.1',
|
|
'0.4.0',
|
|
'0.4.1',
|
|
'0.4.2',
|
|
'0.4.3',
|
|
'0.4.4',
|
|
'0.4.5'
|
|
);
|
|
${encryptedOptions
|
|
.map(
|
|
(option) => /* SQL */ `
|
|
if is_using_old_wrappers then
|
|
select id into v_${option.name} from pgsodium.valid_key where name = '${formState.wrapper_name}_${option.name}' limit 1;
|
|
else
|
|
select id into v_${option.name} from vault.secrets where name = '${formState.wrapper_name}_${option.name}' limit 1;
|
|
end if;
|
|
`
|
|
)
|
|
.join('\n')}
|
|
|
|
execute format(
|
|
E'create server "${formState.server_name}" foreign data wrapper "${formState.wrapper_name}" options (${optionsSqlArray});',
|
|
${encryptedOptions
|
|
.filter((option) => formState[option.name])
|
|
.map((option) => `v_${option.name}`)
|
|
.join(',\n')}
|
|
);
|
|
end $$;
|
|
`
|
|
|
|
const createTablesSql = tables
|
|
.map((newTable) => {
|
|
const columns: AvailableColumn[] = newTable.columns
|
|
|
|
return /* SQL */ `
|
|
create foreign table "${newTable.schema_name}"."${newTable.table_name}" (
|
|
${columns.map((column) => `"${column.name}" ${column.type}`).join(',\n ')}
|
|
)
|
|
server ${formState.server_name}
|
|
options (
|
|
${Object.entries(newTable)
|
|
.filter(
|
|
([key, value]) =>
|
|
key !== 'table_name' &&
|
|
key !== 'schema_name' &&
|
|
key !== 'columns' &&
|
|
key !== 'index' &&
|
|
key !== 'is_new_schema' &&
|
|
Boolean(value)
|
|
)
|
|
.map(([key, value]) => `${key} '${value}'`)
|
|
.join(',\n ')}
|
|
);
|
|
`
|
|
})
|
|
.join('\n\n')
|
|
|
|
const options = [...schemaOptions, "strict 'true'"].join(', ')
|
|
|
|
const importForeignSchemaSql = /* SQL */ `
|
|
import foreign schema "${sourceSchema}" from server ${formState.server_name} into ${targetSchema} options (${options});
|
|
`
|
|
|
|
const sql = /* SQL */ `
|
|
${newSchemasSql}
|
|
|
|
${createWrapperSql}
|
|
|
|
${createEncryptedKeysSql}
|
|
|
|
${createServerSql}
|
|
|
|
${mode === 'tables' ? createTablesSql : ''}
|
|
|
|
${mode === 'schema' ? importForeignSchemaSql : ''}
|
|
`
|
|
|
|
return sql
|
|
}
|
|
|
|
export async function createFDW({ projectRef, connectionString, ...rest }: FDWCreateVariables) {
|
|
const sql = wrapWithTransaction(getCreateFDWSql(rest))
|
|
const { result } = await executeSql({ projectRef, connectionString, sql })
|
|
return result
|
|
}
|
|
|
|
export type FDWCreateData = Awaited<ReturnType<typeof createFDW>>
|
|
|
|
export const useFDWCreateMutation = ({
|
|
onSuccess,
|
|
onError,
|
|
...options
|
|
}: Omit<
|
|
UseCustomMutationOptions<FDWCreateData, ResponseError, FDWCreateVariables>,
|
|
'mutationFn'
|
|
> = {}) => {
|
|
const queryClient = useQueryClient()
|
|
|
|
return useMutation<FDWCreateData, ResponseError, FDWCreateVariables>({
|
|
mutationFn: (vars) => createFDW(vars),
|
|
async onSuccess(data, variables, context) {
|
|
const { projectRef } = variables
|
|
|
|
await Promise.all([
|
|
queryClient.invalidateQueries({ queryKey: fdwKeys.list(projectRef), refetchType: 'all' }),
|
|
queryClient.invalidateQueries({ queryKey: entityTypeKeys.list(projectRef) }),
|
|
queryClient.invalidateQueries({ queryKey: foreignTableKeys.list(projectRef) }),
|
|
queryClient.invalidateQueries({ queryKey: vaultSecretsKeys.list(projectRef) }),
|
|
])
|
|
|
|
await onSuccess?.(data, variables, context)
|
|
},
|
|
async onError(data, variables, context) {
|
|
if (onError === undefined) {
|
|
toast.error(
|
|
`Failed to create ${variables.wrapperMeta.label} foreign data wrapper: ${data.message}`
|
|
)
|
|
} else {
|
|
onError(data, variables, context)
|
|
}
|
|
},
|
|
...options,
|
|
})
|
|
}
|