TableEntity.utils.ts139 lines · main
| 1 | import { SupaTable } from '@/components/grid/types' |
| 2 | import { Lint } from '@/data/lint/lint-query' |
| 3 | |
| 4 | export const getEntityLintDetails = ( |
| 5 | entityName: string, |
| 6 | lintName: string, |
| 7 | lintLevels: ('ERROR' | 'WARN' | 'INFO')[], |
| 8 | lints: Lint[], |
| 9 | schema: string |
| 10 | ): { hasLint: boolean; count: number; matchingLint: Lint | null } => { |
| 11 | const matchingLint = |
| 12 | lints?.find( |
| 13 | (lint) => |
| 14 | lint?.metadata?.name === entityName && |
| 15 | lint?.metadata?.schema === schema && |
| 16 | lint?.name === lintName && |
| 17 | lintLevels.includes(lint?.level) |
| 18 | ) || null |
| 19 | |
| 20 | return { |
| 21 | hasLint: matchingLint !== null, |
| 22 | count: matchingLint ? 1 : 0, |
| 23 | matchingLint, |
| 24 | } |
| 25 | } |
| 26 | |
| 27 | export const getTablePoliciesUrl = ( |
| 28 | projectRef: string | undefined, |
| 29 | schema: string | undefined, |
| 30 | name: string | undefined |
| 31 | ): string => { |
| 32 | return `/project/${projectRef ?? ''}/auth/policies?search=${encodeURIComponent( |
| 33 | name ?? '' |
| 34 | )}&schema=${encodeURIComponent(schema ?? '')}` |
| 35 | } |
| 36 | |
| 37 | export const formatTableRowsToSQL = (table: SupaTable, rows: any[]) => { |
| 38 | if (rows.length === 0) return '' |
| 39 | |
| 40 | const columns = table.columns.map((col) => `"${col.name}"`).join(', ') |
| 41 | |
| 42 | const valuesSets = rows |
| 43 | .map((row) => { |
| 44 | const filteredRow = { ...row } |
| 45 | if ('idx' in filteredRow) delete filteredRow.idx |
| 46 | |
| 47 | const values = Object.entries(filteredRow).map(([key, val]) => { |
| 48 | const { dataType, format } = table.columns.find((col) => col.name === key) ?? {} |
| 49 | |
| 50 | // We only check for NULL, array and JSON types, everything else we stringify |
| 51 | // given that Postgres can implicitly cast the right type based on the column type |
| 52 | // For string types, we need to deal with escaping single quotes |
| 53 | const stringFormats = ['text', 'varchar'] |
| 54 | |
| 55 | if (val === null) { |
| 56 | return 'null' |
| 57 | } else if (dataType === 'ARRAY') { |
| 58 | const array = Array.isArray(val) ? val : JSON.parse(val as string) |
| 59 | return `${formatArrayForSql(array as unknown[])}` |
| 60 | } else if (format?.includes('json')) { |
| 61 | return `${JSON.stringify(val).replace(/\\"/g, '"').replace(/'/g, "''").replace('"', "'").replace(/.$/, "'")}` |
| 62 | } else if ( |
| 63 | typeof format === 'string' && |
| 64 | typeof val === 'string' && |
| 65 | stringFormats.includes(format) |
| 66 | ) { |
| 67 | return `'${val.replaceAll("'", "''")}'` |
| 68 | } else if (typeof val === 'number' || typeof val === 'boolean') { |
| 69 | return `${val}` |
| 70 | } else if (typeof val === 'string') { |
| 71 | return `'${val.replaceAll("'", "''")}'` |
| 72 | } else { |
| 73 | return `'${val}'` |
| 74 | } |
| 75 | }) |
| 76 | |
| 77 | return `(${values.join(', ')})` |
| 78 | }) |
| 79 | .join(', ') |
| 80 | |
| 81 | return `INSERT INTO "${table.schema}"."${table.name}" (${columns}) VALUES ${valuesSets};` |
| 82 | } |
| 83 | |
| 84 | /** |
| 85 | * Generate a random tag for dollar-quoting of SQL strings |
| 86 | * |
| 87 | * @return A random tag in the format `$tag$` |
| 88 | */ |
| 89 | const generateRandomTag = (): `$${string}$` => { |
| 90 | const inner = Math.random().toString(36).substring(2, 15) |
| 91 | // Ensure the tag starts with a character not a digit to avoid conflicts with |
| 92 | // Postgres parameter syntax |
| 93 | return `$x${inner}$` |
| 94 | } |
| 95 | |
| 96 | /** |
| 97 | * Wrap a string in dollar-quote tags, ensuring the tag does not appear in the string |
| 98 | * |
| 99 | * @throws Error if unable to generate a unique dollar-quote tag after multiple attempts |
| 100 | */ |
| 101 | const safeDollarQuote = (str: string): string => { |
| 102 | let tag = generateRandomTag() |
| 103 | |
| 104 | let attempts = 0 |
| 105 | const maxAttempts = 100 |
| 106 | while (str.includes(tag)) { |
| 107 | if (attempts >= maxAttempts) { |
| 108 | throw new Error('Unable to generate a unique dollar-quote tag after multiple attempts.') |
| 109 | } |
| 110 | |
| 111 | attempts++ |
| 112 | tag = generateRandomTag() |
| 113 | } |
| 114 | return `${tag}${str}${tag}` |
| 115 | } |
| 116 | |
| 117 | const formatArrayForSql = (arr: unknown[]): string => { |
| 118 | let result = 'ARRAY[' |
| 119 | |
| 120 | arr.forEach((item, index) => { |
| 121 | if (Array.isArray(item)) { |
| 122 | result += formatArrayForSql(item) |
| 123 | } else if (typeof item === 'string') { |
| 124 | result += `'${item.replaceAll("'", "''")}'` |
| 125 | } else if (!!item && typeof item === 'object') { |
| 126 | result += `${safeDollarQuote(JSON.stringify(item))}::json` |
| 127 | } else { |
| 128 | result += `${item}` |
| 129 | } |
| 130 | |
| 131 | if (index < arr.length - 1) { |
| 132 | result += ',' |
| 133 | } |
| 134 | }) |
| 135 | |
| 136 | result += ']' |
| 137 | |
| 138 | return result |
| 139 | } |