Wrappers.utils.ts241 lines · main
1import * as z from 'zod'
2
3import { WRAPPER_HANDLERS, WRAPPERS } from './Wrappers.constants'
4import type { Table, WrapperMeta } from './Wrappers.types'
5import { FDW, FDWTable } from '@/data/fdw/fdws-query'
6
7const tableSchema = z
8 .object({
9 index: z.number(),
10 columns: z.array(z.object({ name: z.string(), type: z.string() })),
11 is_new_schema: z.boolean(),
12 schema: z.string(),
13 schema_name: z.string(),
14 table_name: z.string(),
15 object: z.any().optional(),
16 })
17 .passthrough() // passthrough is needed for table options
18
19export const getWrapperCreationFormSchema = (wrapperMeta: WrapperMeta) => {
20 let wrapperSchema = {
21 // Common validation for all wrappers
22 wrapper_name: z.string().min(1, 'Please provide a name for your wrapper'),
23 } as Record<string, any>
24
25 // Add wrapper specific options
26 wrapperMeta.server.options.forEach((option) => {
27 if (option.required) {
28 wrapperSchema[option.name] = z.string().min(1, 'Required')
29 return
30 }
31 wrapperSchema[option.name] = z.string().optional()
32 })
33
34 return z.discriminatedUnion('mode', [
35 z
36 .object({
37 mode: z.literal('tables'),
38 tables: z
39 .array(tableSchema, { required_error: 'Please provide at least one table' })
40 .min(1, 'Please provide at least one table'),
41 })
42 .merge(z.object(wrapperSchema)),
43 z
44 .object({
45 mode: z.literal('schema'),
46 source_schema: z.string().min(1, 'Please provide a source schema'),
47 target_schema: z.string().min(1, 'Please provide an unique target schema'),
48 })
49 .merge(z.object(wrapperSchema)),
50 ])
51}
52
53export const getEditionFormSchema = (wrapperMeta: WrapperMeta) => {
54 let wrapperSchema = {
55 // Common validation for all wrappers
56 wrapper_name: z.string().min(1, 'Please provide a name for your wrapper'),
57 tables: z
58 .array(tableSchema, { required_error: 'Please provide at least one table' })
59 .min(1, 'Please provide at least one table'),
60 } as Record<string, any>
61
62 // Add wrapper specific options
63 wrapperMeta.server.options.forEach((option) => {
64 if (option.required) {
65 wrapperSchema[option.name] = z.string().min(1, 'Required')
66 return
67 }
68 wrapperSchema[option.name] = z.string().optional()
69 })
70 return z.object(wrapperSchema)
71}
72
73export const getTableFormSchema = (table: Table) => {
74 let tableSchema = {
75 table_name: z.string().min(1, 'Required'),
76 schema: z.string().min(1, 'Required'),
77 schema_name: z.string().optional(),
78 columns: z.array(
79 z.object({
80 name: z.string().min(1, 'Required'),
81 type: z.string().min(1, 'Required'),
82 })
83 ),
84 } as Record<string, any>
85
86 table.options.forEach((option) => {
87 if (option.required) {
88 tableSchema[option.name] = z.string().min(1, 'Required')
89 return
90 }
91 tableSchema[option.name] = z.string().optional()
92 })
93
94 return (
95 z
96 .object(tableSchema)
97 // passthrough is needed for table options
98 .passthrough()
99 .superRefine((values, ctx) => {
100 if (values.schema === 'custom' && !values.schema_name) {
101 ctx.addIssue({
102 code: 'custom',
103 path: ['schema_name'],
104 message: 'Required',
105 })
106 }
107 })
108 )
109}
110
111export const makeValidateRequired = (options: { name: string; required: boolean }[]) => {
112 const requiredOptionsSet = new Set(
113 options.filter((option) => option.required).map((option) => option.name)
114 )
115
116 const requiredArrayOptionsSet = new Set(
117 Array.from(requiredOptionsSet).filter((option) => option.includes('.'))
118 )
119 const requiredArrayOptions = Array.from(requiredArrayOptionsSet)
120
121 return (values: Record<string, any>) => {
122 const errors = Object.fromEntries(
123 Object.entries(values)
124 .flatMap(([key, value]) =>
125 Array.isArray(value)
126 ? [[key, value], ...value.map((v, i) => [`${key}.${i}`, v])]
127 : [[key, value]]
128 )
129 .filter(([_key, value]) => {
130 const [key, idx] = _key.split('.')
131
132 if (
133 idx !== undefined &&
134 requiredOptionsSet.has(key) &&
135 Object.keys(value).some((subKey) => requiredArrayOptionsSet.has(`${key}.${subKey}`))
136 ) {
137 const arrayOption = requiredArrayOptions.find((option) => option.startsWith(`${key}.`))
138 if (arrayOption) {
139 const subKey = arrayOption.split('.')[1]
140 return !value[subKey]
141 }
142
143 return false
144 }
145
146 return requiredOptionsSet.has(key) && (Array.isArray(value) ? value.length < 1 : !value)
147 })
148 .map(([key]) => {
149 if (key === 'table_name') return [key, 'Please provide a name for your table']
150 else if (key === 'columns') return [key, 'Please select at least one column']
151 else return [key, 'This field is required']
152 })
153 )
154
155 return errors
156 }
157}
158
159export const NewTable = {} as FormattedWrapperTable
160
161export interface FormattedWrapperTable {
162 index: number
163 columns: { name: string }[]
164 is_new_schema: boolean
165 schema: string
166 schema_name: string
167 table_name: string
168 object?: string // From options object for Firebase/Stripe
169 [key: string]: any // For other dynamic options from table.options
170}
171
172export const formatWrapperTables = (
173 wrapper: { handler: string; tables?: FDWTable[] },
174 wrapperMeta?: WrapperMeta
175): FormattedWrapperTable[] => {
176 const tables = wrapper?.tables ?? []
177
178 return tables.map((table) => {
179 let index: number = 0
180 const options = Object.fromEntries(table.options.map((option: string) => option.split('=')))
181
182 switch (wrapper.handler) {
183 case WRAPPER_HANDLERS.STRIPE:
184 index =
185 wrapperMeta?.tables.findIndex(
186 (x) => x.options.find((x) => x.name === 'object')?.defaultValue === options.object
187 ) ?? 0
188 break
189 case WRAPPER_HANDLERS.FIREBASE:
190 if (options.object === 'auth/users') {
191 index =
192 wrapperMeta?.tables.findIndex((x) =>
193 x.options.find((x) => x.defaultValue === 'auth/users')
194 ) ?? 0
195 } else {
196 index = wrapperMeta?.tables.findIndex((x) => x.label === 'Firestore Collection') ?? 0
197 }
198 break
199 case WRAPPER_HANDLERS.S3:
200 case WRAPPER_HANDLERS.AIRTABLE:
201 case WRAPPER_HANDLERS.LOGFLARE:
202 case WRAPPER_HANDLERS.BIG_QUERY:
203 case WRAPPER_HANDLERS.CLICK_HOUSE:
204 break
205 }
206
207 return {
208 ...options,
209 index,
210 id: table.id,
211 columns: table.columns ?? [],
212 is_new_schema: false,
213 schema: table.schema,
214 schema_name: table.schema,
215 table_name: table.name,
216 }
217 })
218}
219
220export const convertKVStringArrayToJson = (values: string[]): Record<string, string> => {
221 return Object.fromEntries(values.map((value) => value.split('=')))
222}
223
224export function wrapperMetaComparator(
225 wrapperMeta: Pick<WrapperMeta, 'handlerName' | 'server'>,
226 wrapper: FDW | undefined
227) {
228 if (wrapperMeta.handlerName === 'wasm_fdw_handler') {
229 const serverOptions = convertKVStringArrayToJson(wrapper?.server_options ?? [])
230 return (
231 wrapperMeta.server.options.find((option) => option.name === 'fdw_package_name')
232 ?.defaultValue === serverOptions['fdw_package_name']
233 )
234 }
235
236 return wrapperMeta.handlerName === wrapper?.handler
237}
238
239export function getWrapperMetaForWrapper(wrapper: FDW | undefined) {
240 return WRAPPERS.find((w) => wrapperMetaComparator(w, wrapper))
241}