TableEntity.utils.ts 4.1 KB

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