TableEntity.utils.test.ts 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. import { describe, expect, it } from 'vitest'
  2. import { formatTableRowsToSQL, getTablePoliciesUrl } from './TableEntity.utils'
  3. import type { SupaTable } from '@/components/grid/types'
  4. import { ENTITY_TYPE } from '@/data/entity-types/entity-type-constants'
  5. describe('TableEntity.utils: formatTableRowsToSQL', () => {
  6. it('should format rows into a single SQL INSERT statement', () => {
  7. const table: SupaTable = {
  8. id: 1,
  9. type: ENTITY_TYPE.TABLE,
  10. columns: [
  11. { name: 'id', dataType: 'bigint', format: 'int8', position: 0 },
  12. { name: 'name', dataType: 'text', format: 'text', position: 1 },
  13. ],
  14. name: 'people',
  15. schema: 'public',
  16. comment: undefined,
  17. estimateRowCount: 1,
  18. }
  19. const rows = [
  20. { id: 1, name: 'Person 1' },
  21. { id: 2, name: 'Person 2' },
  22. { id: 3, name: 'Person 3' },
  23. ]
  24. const result = formatTableRowsToSQL(table, rows)
  25. const expected = `INSERT INTO "public"."people" ("id", "name") VALUES (1, 'Person 1'), (2, 'Person 2'), (3, 'Person 3');`
  26. expect(result).toBe(expected)
  27. })
  28. it('should not stringify null values', () => {
  29. const table: SupaTable = {
  30. id: 1,
  31. type: ENTITY_TYPE.TABLE,
  32. columns: [
  33. { name: 'id', dataType: 'bigint', format: 'int8', position: 0 },
  34. { name: 'name', dataType: 'text', format: 'text', position: 1 },
  35. ],
  36. name: 'people',
  37. schema: 'public',
  38. comment: undefined,
  39. estimateRowCount: 1,
  40. }
  41. const rows = [
  42. { id: 1, name: 'Person 1' },
  43. { id: 2, name: null },
  44. { id: 3, name: 'Person 3' },
  45. ]
  46. const result = formatTableRowsToSQL(table, rows)
  47. const expected = `INSERT INTO "public"."people" ("id", "name") VALUES (1, 'Person 1'), (2, null), (3, 'Person 3');`
  48. expect(result).toBe(expected)
  49. })
  50. it('should handle PG JSON and array columns', () => {
  51. const table: SupaTable = {
  52. id: 1,
  53. type: ENTITY_TYPE.TABLE,
  54. columns: [
  55. { name: 'id', dataType: 'bigint', format: 'int8', position: 0 },
  56. { name: 'name', dataType: 'text', format: 'text', position: 1 },
  57. { name: 'tags', dataType: 'ARRAY', format: '_text', position: 2 },
  58. { name: 'metadata', dataType: 'jsonb', format: 'jsonb', position: 3 },
  59. ],
  60. name: 'demo',
  61. schema: 'public',
  62. comment: undefined,
  63. estimateRowCount: 1,
  64. }
  65. const rows = [
  66. {
  67. idx: 1,
  68. id: 2,
  69. name: 'Person 1',
  70. tags: ['tag-a', 'tag-c'],
  71. metadata: '{"version": 1}',
  72. },
  73. {
  74. idx: 2,
  75. id: 3,
  76. name: 'ONeil',
  77. tags: ['tag-a'],
  78. metadata: `{"version": 1, "name": "O'Neil"}`,
  79. },
  80. ]
  81. const result = formatTableRowsToSQL(table, rows)
  82. const expected = `INSERT INTO "public"."demo" ("id", "name", "tags", "metadata") VALUES (2, 'Person 1', ARRAY['tag-a','tag-c'], '{"version": 1}'), (3, 'ONeil', ARRAY['tag-a'], '{"version": 1, "name": "O''Neil"}');`
  83. expect(result).toBe(expected)
  84. })
  85. it('should emit valid Postgres literals for booleans, numbers and text arrays', () => {
  86. const table: SupaTable = {
  87. id: 1,
  88. type: ENTITY_TYPE.TABLE,
  89. columns: [
  90. { name: 'id', dataType: 'text', format: 'text', position: 0 },
  91. { name: 'public', dataType: 'bool', format: 'bool', position: 1 },
  92. { name: 'avif_autodetection', dataType: 'bool', format: 'bool', position: 2 },
  93. { name: 'file_size_limit', dataType: 'int8', format: 'int8', position: 3 },
  94. { name: 'allowed_mime_types', dataType: 'ARRAY', format: '_text', position: 4 },
  95. ],
  96. name: 'buckets',
  97. schema: 'storage',
  98. comment: undefined,
  99. estimateRowCount: 1,
  100. }
  101. const rows = [
  102. {
  103. id: 'emails',
  104. public: true,
  105. avif_autodetection: false,
  106. file_size_limit: 10485760,
  107. allowed_mime_types: ['image/*', "image/o'neil"],
  108. },
  109. ]
  110. const result = formatTableRowsToSQL(table, rows)
  111. const expected = `INSERT INTO "storage"."buckets" ("id", "public", "avif_autodetection", "file_size_limit", "allowed_mime_types") VALUES ('emails', true, false, 10485760, ARRAY['image/*','image/o''neil']);`
  112. expect(result).toBe(expected)
  113. })
  114. it('should escape fallback string formats outside text and varchar', () => {
  115. const table: SupaTable = {
  116. id: 1,
  117. type: ENTITY_TYPE.TABLE,
  118. columns: [{ name: 'email', dataType: 'USER-DEFINED', format: 'citext', position: 0 }],
  119. name: 'users',
  120. schema: 'public',
  121. comment: undefined,
  122. estimateRowCount: 1,
  123. }
  124. const rows = [{ email: "o'neil@example.com" }]
  125. const result = formatTableRowsToSQL(table, rows)
  126. const expected = `INSERT INTO "public"."users" ("email") VALUES ('o''neil@example.com');`
  127. expect(result).toBe(expected)
  128. })
  129. it('should return an empty string for empty rows', () => {
  130. const table: SupaTable = {
  131. id: 1,
  132. type: ENTITY_TYPE.TABLE,
  133. columns: [
  134. { name: 'id', dataType: 'bigint', format: 'int8', position: 0 },
  135. { name: 'name', dataType: 'text', format: 'text', position: 1 },
  136. ],
  137. name: 'people',
  138. schema: 'public',
  139. comment: undefined,
  140. estimateRowCount: 1,
  141. }
  142. const result = formatTableRowsToSQL(table, [])
  143. expect(result).toBe('')
  144. })
  145. it('should remove the idx property', () => {
  146. const table: SupaTable = {
  147. id: 1,
  148. type: ENTITY_TYPE.TABLE,
  149. columns: [
  150. { name: 'id', dataType: 'bigint', format: 'int8', position: 0 },
  151. { name: 'name', dataType: 'text', format: 'text', position: 1 },
  152. ],
  153. name: 'people',
  154. schema: 'public',
  155. comment: undefined,
  156. estimateRowCount: 1,
  157. }
  158. const rows = [
  159. { idx: 0, id: 1, name: 'Person 1' },
  160. { idx: 1, id: 2, name: 'Person 2' },
  161. ]
  162. const result = formatTableRowsToSQL(table, rows)
  163. const expected = `INSERT INTO "public"."people" ("id", "name") VALUES (1, 'Person 1'), (2, 'Person 2');`
  164. expect(result).toBe(expected)
  165. })
  166. })
  167. describe('TableEntity.utils: getTablePoliciesUrl', () => {
  168. it('builds the policies url for plain schema and name values', () => {
  169. expect(getTablePoliciesUrl('abc', 'public', 'users')).toBe(
  170. '/project/abc/auth/policies?search=users&schema=public'
  171. )
  172. })
  173. it('preserves special characters in the table name', () => {
  174. const url = getTablePoliciesUrl('abc', 'public', 'user_data&secret=1')
  175. const parsed = new URL(url, 'http://example.com')
  176. expect(parsed.searchParams.get('search')).toBe('user_data&secret=1')
  177. expect(parsed.searchParams.get('schema')).toBe('public')
  178. })
  179. it('preserves special characters in the schema', () => {
  180. const url = getTablePoliciesUrl('abc', 'my schema+x', 'users')
  181. const parsed = new URL(url, 'http://example.com')
  182. expect(parsed.searchParams.get('schema')).toBe('my schema+x')
  183. expect(parsed.searchParams.get('search')).toBe('users')
  184. })
  185. it('encodes both the table name and schema together', () => {
  186. const url = getTablePoliciesUrl('abc', 'a&b=c', 'd e+f')
  187. const parsed = new URL(url, 'http://example.com')
  188. expect(parsed.searchParams.get('search')).toBe('d e+f')
  189. expect(parsed.searchParams.get('schema')).toBe('a&b=c')
  190. })
  191. })