| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523 |
- import { FOREIGN_KEY_CASCADE_ACTION, safeSql } from '@supabase/pg-meta'
- import { beforeEach, describe, expect, it, vi } from 'vitest'
- import type { ForeignKey } from './ForeignKeySelector/ForeignKeySelector.types'
- import type { ColumnField } from './SidePanelEditor.types'
- import { createTable } from './SidePanelEditor.utils'
- // Define mock functions at module level
- const mockExecuteSql = vi.fn()
- const mockGetTable = vi.fn()
- const mockSendEvent = vi.fn()
- const mockPrefetchEditorTablePage = vi.fn()
- const mockToastLoading = vi.fn()
- const mockToastSuccess = vi.fn()
- const mockToastError = vi.fn()
- const mockFetchQuery = vi.fn()
- // Setup mocks before imports
- vi.mock('@/data/query-client', () => ({
- getQueryClient: () => ({
- fetchQuery: mockFetchQuery,
- }),
- }))
- vi.mock('@/data/sql/execute-sql-query', () => ({
- executeSql: (...args: unknown[]) => mockExecuteSql(...args),
- }))
- vi.mock('@/data/tables/table-retrieve-query', () => ({
- getTable: (...args: unknown[]) => mockGetTable(...args),
- getTableQuery: (...args: unknown[]) => mockGetTable(...args),
- }))
- vi.mock('@/data/telemetry/send-event-mutation', () => ({
- sendEvent: (...args: unknown[]) => mockSendEvent(...args),
- }))
- vi.mock('@/data/prefetchers/project.$ref.editor.$id', () => ({
- prefetchEditorTablePage: (...args: unknown[]) => mockPrefetchEditorTablePage(...args),
- }))
- vi.mock('sonner', () => ({
- toast: {
- loading: (...args: unknown[]) => mockToastLoading(...args),
- success: (...args: unknown[]) => mockToastSuccess(...args),
- error: (...args: unknown[]) => mockToastError(...args),
- },
- }))
- // Mock SparkBar component used in toast
- vi.mock('@/components/ui/SparkBar', () => ({
- default: () => null,
- }))
- // Helper to create a column field with defaults
- const createColumnField = (overrides: Partial<ColumnField> = {}): ColumnField => ({
- id: 'col-1',
- name: 'column',
- table: 'test_table',
- schema: 'public',
- format: 'text',
- check: null,
- comment: null,
- defaultValue: null,
- isNullable: true,
- isUnique: false,
- isArray: false,
- isIdentity: false,
- isPrimaryKey: false,
- isNewColumn: true,
- isEncrypted: false,
- ...overrides,
- })
- describe('createTable', () => {
- const projectRef = 'test-project-ref'
- const connectionString = 'postgresql://localhost:5432/test'
- const toastId = 'test-toast-id'
- const basePayload = {
- name: 'test_table',
- schema: 'public',
- comment: 'A test table',
- }
- const mockTableResult = {
- id: 123,
- name: 'test_table',
- schema: 'public',
- comment: 'A test table',
- columns: [],
- primary_keys: [],
- relationships: [],
- }
- beforeEach(() => {
- vi.clearAllMocks()
- // Default mock implementations
- mockExecuteSql.mockResolvedValue({ result: [] })
- mockGetTable.mockResolvedValue(mockTableResult)
- mockSendEvent.mockResolvedValue({})
- mockPrefetchEditorTablePage.mockResolvedValue(undefined)
- mockFetchQuery.mockImplementation(({ queryFn }) => {
- if (queryFn) {
- return queryFn({ signal: new AbortController().signal })
- }
- return Promise.resolve(mockTableResult)
- })
- })
- it('should create a basic table with no columns', async () => {
- const result = await createTable({
- projectRef,
- connectionString,
- toastId,
- payload: basePayload,
- columns: [],
- foreignKeyRelations: [],
- isRLSEnabled: false,
- })
- expect(mockExecuteSql).toHaveBeenCalledTimes(1)
- expect(mockExecuteSql).toHaveBeenCalledWith(
- expect.objectContaining({
- projectRef,
- connectionString,
- queryKey: ['table', 'create-with-columns'],
- })
- )
- // Should show loading toast
- expect(mockToastLoading).toHaveBeenCalledWith(`Creating table ${basePayload.name}...`, {
- id: toastId,
- })
- // Should track table creation event
- expect(mockSendEvent).toHaveBeenCalledWith({
- event: {
- action: 'table_created',
- properties: {
- has_generated_policies: false,
- method: 'table_editor',
- schema_name: 'public',
- table_name: 'test_table',
- },
- groups: {
- project: projectRef,
- },
- },
- })
- // Should prefetch the editor table page
- expect(mockPrefetchEditorTablePage).toHaveBeenCalledWith(
- expect.objectContaining({
- projectRef,
- connectionString,
- id: mockTableResult.id,
- })
- )
- expect(result).toStrictEqual({
- failedPolicies: [],
- table: mockTableResult,
- })
- })
- it('should create a table with RLS enabled', async () => {
- await createTable({
- projectRef,
- connectionString,
- toastId,
- payload: basePayload,
- columns: [],
- foreignKeyRelations: [],
- isRLSEnabled: true,
- })
- const sqlCall = mockExecuteSql.mock.calls[0][0]
- expect(sqlCall.sql).toContain('ENABLE ROW LEVEL SECURITY')
- expect(mockSendEvent).toHaveBeenCalledWith({
- event: {
- action: 'table_rls_enabled',
- properties: {
- method: 'table_editor',
- schema_name: 'public',
- table_name: 'test_table',
- },
- groups: {
- project: projectRef,
- },
- },
- })
- })
- it('should create a table with columns', async () => {
- const columns: ColumnField[] = [
- createColumnField({
- id: 'col-1',
- name: 'id',
- format: 'int8',
- isNullable: false,
- isIdentity: true,
- isPrimaryKey: true,
- }),
- createColumnField({
- id: 'col-2',
- name: 'name',
- format: 'text',
- comment: 'User name',
- }),
- ]
- await createTable({
- projectRef,
- connectionString,
- toastId,
- payload: basePayload,
- columns,
- foreignKeyRelations: [],
- isRLSEnabled: false,
- })
- const sqlCall = mockExecuteSql.mock.calls[0][0]
- expect(sqlCall.sql).toContain('ALTER TABLE')
- expect(sqlCall.sql).toContain('ADD COLUMN')
- expect(sqlCall.sql).toContain('ADD PRIMARY KEY')
- expect(sqlCall.sql).toContain('id')
- })
- it('should create a table with composite primary key', async () => {
- const columns: ColumnField[] = [
- createColumnField({
- id: 'col-1',
- name: 'user_id',
- format: 'int8',
- isNullable: false,
- isPrimaryKey: true,
- }),
- createColumnField({
- id: 'col-2',
- name: 'order_id',
- format: 'int8',
- isNullable: false,
- isPrimaryKey: true,
- }),
- ]
- await createTable({
- projectRef,
- connectionString,
- toastId,
- payload: basePayload,
- columns,
- foreignKeyRelations: [],
- isRLSEnabled: false,
- })
- const sqlCall = mockExecuteSql.mock.calls[0][0]
- expect(sqlCall.sql).toContain('ADD PRIMARY KEY')
- expect(sqlCall.sql).toContain('user_id')
- expect(sqlCall.sql).toContain('order_id')
- })
- it('should create a table with foreign key relations', async () => {
- const columns: ColumnField[] = [
- createColumnField({
- id: 'col-1',
- name: 'id',
- format: 'int8',
- isNullable: false,
- isIdentity: true,
- isPrimaryKey: true,
- }),
- createColumnField({
- id: 'col-2',
- name: 'user_id',
- format: 'int8',
- isNullable: false,
- }),
- ]
- const foreignKeyRelations: ForeignKey[] = [
- {
- schema: 'public',
- table: 'users',
- columns: [{ source: 'user_id', target: 'id' }],
- deletionAction: FOREIGN_KEY_CASCADE_ACTION.CASCADE,
- updateAction: FOREIGN_KEY_CASCADE_ACTION.NO_ACTION,
- },
- ]
- await createTable({
- projectRef,
- connectionString,
- toastId,
- payload: basePayload,
- columns,
- foreignKeyRelations,
- isRLSEnabled: false,
- })
- const sqlCall = mockExecuteSql.mock.calls[0][0]
- expect(sqlCall.sql).toContain('ADD FOREIGN KEY')
- expect(sqlCall.sql).toContain('REFERENCES')
- expect(sqlCall.sql).toContain('users')
- expect(sqlCall.sql).toContain('ON DELETE CASCADE')
- })
- it('should include organization slug in telemetry when provided', async () => {
- const organizationSlug = 'test-org'
- await createTable({
- projectRef,
- connectionString,
- toastId,
- payload: basePayload,
- columns: [],
- foreignKeyRelations: [],
- isRLSEnabled: true,
- organizationSlug,
- })
- expect(mockSendEvent).toHaveBeenCalledWith({
- event: expect.objectContaining({
- action: 'table_created',
- groups: {
- project: projectRef,
- organization: organizationSlug,
- },
- }),
- })
- expect(mockSendEvent).toHaveBeenCalledWith({
- event: expect.objectContaining({
- action: 'table_rls_enabled',
- groups: {
- project: projectRef,
- organization: organizationSlug,
- },
- }),
- })
- })
- it('should handle telemetry errors gracefully', async () => {
- const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
- mockSendEvent.mockRejectedValue(new Error('Telemetry failed'))
- const result = await createTable({
- projectRef,
- connectionString,
- toastId,
- payload: basePayload,
- columns: [],
- foreignKeyRelations: [],
- isRLSEnabled: false,
- })
- expect(result).toStrictEqual({
- failedPolicies: [],
- table: mockTableResult,
- })
- expect(consoleErrorSpy).toHaveBeenCalledWith(
- 'Failed to track table creation event:',
- expect.any(Error)
- )
- consoleErrorSpy.mockRestore()
- })
- it('should create a table with nullable connectionString', async () => {
- await createTable({
- projectRef,
- connectionString: null,
- toastId,
- payload: basePayload,
- columns: [],
- foreignKeyRelations: [],
- isRLSEnabled: false,
- })
- expect(mockExecuteSql).toHaveBeenCalledWith(
- expect.objectContaining({
- projectRef,
- connectionString: null,
- })
- )
- })
- it('should create table with column having default value', async () => {
- const columns: ColumnField[] = [
- createColumnField({
- name: 'status',
- defaultValue: "'pending'",
- isNullable: false,
- }),
- ]
- await createTable({
- projectRef,
- connectionString,
- toastId,
- payload: basePayload,
- columns,
- foreignKeyRelations: [],
- isRLSEnabled: false,
- })
- expect(mockExecuteSql).toHaveBeenCalledTimes(1)
- })
- it('should create table with unique column', async () => {
- const columns: ColumnField[] = [
- createColumnField({
- name: 'email',
- isNullable: false,
- isUnique: true,
- }),
- ]
- await createTable({
- projectRef,
- connectionString,
- toastId,
- payload: basePayload,
- columns,
- foreignKeyRelations: [],
- isRLSEnabled: false,
- })
- expect(mockExecuteSql).toHaveBeenCalledTimes(1)
- })
- it('should create table with array column', async () => {
- const columns: ColumnField[] = [
- createColumnField({
- name: 'tags',
- isArray: true,
- }),
- ]
- await createTable({
- projectRef,
- connectionString,
- toastId,
- payload: basePayload,
- columns,
- foreignKeyRelations: [],
- isRLSEnabled: false,
- })
- expect(mockExecuteSql).toHaveBeenCalledTimes(1)
- })
- it('should create table with check constraint', async () => {
- const columns: ColumnField[] = [
- createColumnField({
- name: 'age',
- format: 'int4',
- check: safeSql`age >= 0`,
- isNullable: false,
- }),
- ]
- await createTable({
- projectRef,
- connectionString,
- toastId,
- payload: basePayload,
- columns,
- foreignKeyRelations: [],
- isRLSEnabled: false,
- })
- expect(mockExecuteSql).toHaveBeenCalledTimes(1)
- })
- it('should propagate SQL execution errors', async () => {
- mockExecuteSql.mockRejectedValue(new Error('SQL execution failed'))
- await expect(
- createTable({
- projectRef,
- connectionString,
- toastId,
- payload: basePayload,
- columns: [],
- foreignKeyRelations: [],
- isRLSEnabled: false,
- })
- ).rejects.toThrow('SQL execution failed')
- })
- it('should create table in non-public schema', async () => {
- const customSchemaPayload = {
- name: 'custom_table',
- schema: 'private',
- comment: 'A private table',
- }
- await createTable({
- projectRef,
- connectionString,
- toastId,
- payload: customSchemaPayload,
- columns: [],
- foreignKeyRelations: [],
- isRLSEnabled: false,
- })
- const sqlCall = mockExecuteSql.mock.calls[0][0]
- expect(sqlCall.sql).toMatch(/private\.custom_table|"private"\."custom_table"/)
- expect(mockSendEvent).toHaveBeenCalledWith({
- event: expect.objectContaining({
- properties: expect.objectContaining({
- schema_name: 'private',
- }),
- }),
- })
- })
- })
|