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 // 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 // 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 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) => { 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 => { return Object.fromEntries(values.map((value) => value.split('='))) } export function wrapperMetaComparator( wrapperMeta: Pick, 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 }