mirror of
https://github.com/supabase/supabase.git
synced 2026-06-13 01:39:53 +08:00
- fix install badge state in wrappers detail page - add "Install" button in top action bar - "Install wrapper" if required extensions _aren't_ installed - "Add new wrapper" if required extensions _are_ installed - make wrappers "one-click install" by - showing the required extensions in the CreateWrappersSheet - and automatically installing them on wrapper submission Only available behind `isMarketplaceEnabled` flag at the moment. https://github.com/user-attachments/assets/38f5549d-938e-4e2f-a723-53b9a028e9dc
269 lines
8.4 KiB
TypeScript
269 lines
8.4 KiB
TypeScript
import * as z from 'zod'
|
|
|
|
import { WRAPPER_HANDLERS, WRAPPERS } from './Wrappers.constants'
|
|
import type { Table, WrapperMeta } from './Wrappers.types'
|
|
import { FDW, FDWTable } from '@/data/fdw/fdws-query'
|
|
|
|
const tableSchema = z
|
|
.object({
|
|
index: z.number(),
|
|
columns: z.array(z.object({ name: z.string(), type: z.string() })),
|
|
is_new_schema: z.boolean(),
|
|
schema: z.string(),
|
|
schema_name: z.string(),
|
|
table_name: z.string(),
|
|
object: z.any().optional(),
|
|
})
|
|
.passthrough() // passthrough is needed for table options
|
|
|
|
export const getWrapperCreationFormSchema = (wrapperMeta: WrapperMeta) => {
|
|
let wrapperSchema = {
|
|
// Common validation for all wrappers
|
|
wrapper_name: z.string().min(1, 'Please provide a name for your wrapper'),
|
|
} as Record<string, any>
|
|
|
|
// Add wrapper specific options
|
|
wrapperMeta.server.options.forEach((option) => {
|
|
if (option.required) {
|
|
wrapperSchema[option.name] = z.string().min(1, 'Required')
|
|
return
|
|
}
|
|
wrapperSchema[option.name] = z.string().optional()
|
|
})
|
|
|
|
return z.discriminatedUnion('mode', [
|
|
z
|
|
.object({
|
|
mode: z.literal('tables'),
|
|
tables: z
|
|
.array(tableSchema, { required_error: 'Please provide at least one table' })
|
|
.min(1, 'Please provide at least one table'),
|
|
})
|
|
.merge(z.object(wrapperSchema)),
|
|
z
|
|
.object({
|
|
mode: z.literal('schema'),
|
|
source_schema: z.string().min(1, 'Please provide a source schema'),
|
|
target_schema: z.string().min(1, 'Please provide an unique target schema'),
|
|
})
|
|
.merge(z.object(wrapperSchema)),
|
|
])
|
|
}
|
|
|
|
export const getEditionFormSchema = (wrapperMeta: WrapperMeta) => {
|
|
let wrapperSchema = {
|
|
// Common validation for all wrappers
|
|
wrapper_name: z.string().min(1, 'Please provide a name for your wrapper'),
|
|
tables: z
|
|
.array(tableSchema, { required_error: 'Please provide at least one table' })
|
|
.min(1, 'Please provide at least one table'),
|
|
} as Record<string, any>
|
|
|
|
// Add wrapper specific options
|
|
wrapperMeta.server.options.forEach((option) => {
|
|
if (option.required) {
|
|
wrapperSchema[option.name] = z.string().min(1, 'Required')
|
|
return
|
|
}
|
|
wrapperSchema[option.name] = z.string().optional()
|
|
})
|
|
return z.object(wrapperSchema)
|
|
}
|
|
|
|
export const getTableFormSchema = (table: Table) => {
|
|
let tableSchema = {
|
|
table_name: z.string().min(1, 'Required'),
|
|
schema: z.string().min(1, 'Required'),
|
|
schema_name: z.string().optional(),
|
|
columns: z.array(
|
|
z.object({
|
|
name: z.string().min(1, 'Required'),
|
|
type: z.string().min(1, 'Required'),
|
|
})
|
|
),
|
|
} as Record<string, any>
|
|
|
|
table.options.forEach((option) => {
|
|
if (option.required) {
|
|
tableSchema[option.name] = z.string().min(1, 'Required')
|
|
return
|
|
}
|
|
tableSchema[option.name] = z.string().optional()
|
|
})
|
|
|
|
return (
|
|
z
|
|
.object(tableSchema)
|
|
// passthrough is needed for table options
|
|
.passthrough()
|
|
.superRefine((values, ctx) => {
|
|
if (values.schema === 'custom' && !values.schema_name) {
|
|
ctx.addIssue({
|
|
code: 'custom',
|
|
path: ['schema_name'],
|
|
message: 'Required',
|
|
})
|
|
}
|
|
})
|
|
)
|
|
}
|
|
|
|
export const makeValidateRequired = (options: { name: string; required: boolean }[]) => {
|
|
const requiredOptionsSet = new Set(
|
|
options.filter((option) => option.required).map((option) => option.name)
|
|
)
|
|
|
|
const requiredArrayOptionsSet = new Set(
|
|
Array.from(requiredOptionsSet).filter((option) => option.includes('.'))
|
|
)
|
|
const requiredArrayOptions = Array.from(requiredArrayOptionsSet)
|
|
|
|
return (values: Record<string, any>) => {
|
|
const errors = Object.fromEntries(
|
|
Object.entries(values)
|
|
.flatMap(([key, value]) =>
|
|
Array.isArray(value)
|
|
? [[key, value], ...value.map((v, i) => [`${key}.${i}`, v])]
|
|
: [[key, value]]
|
|
)
|
|
.filter(([_key, value]) => {
|
|
const [key, idx] = _key.split('.')
|
|
|
|
if (
|
|
idx !== undefined &&
|
|
requiredOptionsSet.has(key) &&
|
|
Object.keys(value).some((subKey) => requiredArrayOptionsSet.has(`${key}.${subKey}`))
|
|
) {
|
|
const arrayOption = requiredArrayOptions.find((option) => option.startsWith(`${key}.`))
|
|
if (arrayOption) {
|
|
const subKey = arrayOption.split('.')[1]
|
|
return !value[subKey]
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
return requiredOptionsSet.has(key) && (Array.isArray(value) ? value.length < 1 : !value)
|
|
})
|
|
.map(([key]) => {
|
|
if (key === 'table_name') return [key, 'Please provide a name for your table']
|
|
else if (key === 'columns') return [key, 'Please select at least one column']
|
|
else return [key, 'This field is required']
|
|
})
|
|
)
|
|
|
|
return errors
|
|
}
|
|
}
|
|
|
|
export const NewTable = {} as FormattedWrapperTable
|
|
|
|
export interface FormattedWrapperTable {
|
|
index: number
|
|
columns: { name: string }[]
|
|
is_new_schema: boolean
|
|
schema: string
|
|
schema_name: string
|
|
table_name: string
|
|
object?: string // From options object for Firebase/Stripe
|
|
[key: string]: any // For other dynamic options from table.options
|
|
}
|
|
|
|
export const formatWrapperTables = (
|
|
wrapper: { handler: string; tables?: FDWTable[] },
|
|
wrapperMeta?: WrapperMeta
|
|
): FormattedWrapperTable[] => {
|
|
const tables = wrapper?.tables ?? []
|
|
|
|
return tables.map((table) => {
|
|
let index: number = 0
|
|
const options = Object.fromEntries(table.options.map((option: string) => option.split('=')))
|
|
|
|
switch (wrapper.handler) {
|
|
case WRAPPER_HANDLERS.STRIPE:
|
|
index =
|
|
wrapperMeta?.tables.findIndex(
|
|
(x) => x.options.find((x) => x.name === 'object')?.defaultValue === options.object
|
|
) ?? 0
|
|
break
|
|
case WRAPPER_HANDLERS.FIREBASE:
|
|
if (options.object === 'auth/users') {
|
|
index =
|
|
wrapperMeta?.tables.findIndex((x) =>
|
|
x.options.find((x) => x.defaultValue === 'auth/users')
|
|
) ?? 0
|
|
} else {
|
|
index = wrapperMeta?.tables.findIndex((x) => x.label === 'Firestore Collection') ?? 0
|
|
}
|
|
break
|
|
case WRAPPER_HANDLERS.S3:
|
|
case WRAPPER_HANDLERS.AIRTABLE:
|
|
case WRAPPER_HANDLERS.LOGFLARE:
|
|
case WRAPPER_HANDLERS.BIG_QUERY:
|
|
case WRAPPER_HANDLERS.CLICK_HOUSE:
|
|
break
|
|
}
|
|
|
|
return {
|
|
...options,
|
|
index,
|
|
id: table.id,
|
|
columns: table.columns ?? [],
|
|
is_new_schema: false,
|
|
schema: table.schema,
|
|
schema_name: table.schema,
|
|
table_name: table.name,
|
|
}
|
|
})
|
|
}
|
|
|
|
export const convertKVStringArrayToJson = (values: string[]): Record<string, string> => {
|
|
return Object.fromEntries(values.map((value) => value.split('=')))
|
|
}
|
|
|
|
export function wrapperMetaComparator(
|
|
wrapperMeta: Pick<WrapperMeta, 'handlerName' | 'server'>,
|
|
wrapper: FDW | undefined
|
|
) {
|
|
if (wrapperMeta.handlerName === 'wasm_fdw_handler') {
|
|
const serverOptions = convertKVStringArrayToJson(wrapper?.server_options ?? [])
|
|
return (
|
|
wrapperMeta.server.options.find((option) => option.name === 'fdw_package_name')
|
|
?.defaultValue === serverOptions['fdw_package_name']
|
|
)
|
|
}
|
|
|
|
return wrapperMeta.handlerName === wrapper?.handler
|
|
}
|
|
|
|
export function getWrapperMetaForWrapper(wrapper: FDW | undefined) {
|
|
return WRAPPERS.find((w) => wrapperMetaComparator(w, wrapper))
|
|
}
|
|
|
|
/**
|
|
* Returns the subset of extensions that are required but not yet installed.
|
|
* Returns `null` when the extensions list is still loading (undefined),
|
|
* so callers can distinguish "loading" from "nothing to install".
|
|
* Generic so the full extension type (including `default_version`) is preserved.
|
|
*/
|
|
export function getRequiredExtensionsToInstall<
|
|
T extends { name: string; installed_version: string | null },
|
|
>(extensions: T[] | undefined, requiredExtensionNames: string[]): T[] | null {
|
|
if (extensions === undefined) return null
|
|
return extensions.filter(
|
|
(ext) => requiredExtensionNames.includes(ext.name) && !ext.installed_version
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Returns true when the wrappers extension is installed at >= 0.5.0,
|
|
* which is the minimum version required for IMPORT FOREIGN SCHEMA support.
|
|
*/
|
|
export function hasForeignSchemaSupport(
|
|
wrappersExtension: { installed_version: string | null } | undefined
|
|
): boolean {
|
|
return wrappersExtension?.installed_version
|
|
? wrappersExtension.installed_version >= '0.5.0'
|
|
: false
|
|
}
|