Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion apps/sim/lib/table/events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ beforeAll(() => {
afterAll(resetEnvMock)

import type { TableEvent } from '@/lib/table/events'
import { appendTableEvent, getLatestTableEventId, readTableEventsSince } from '@/lib/table/events'
import {
appendTableEvent,
getLatestTableEventId,
readTableEventsSince,
signalTableViewsChanged,
} from '@/lib/table/events'

/** Module-level memory buffer can't be reset without vi.resetModules — use a
* unique tableId per test to avoid cross-test bleed. */
Expand Down Expand Up @@ -75,3 +80,23 @@ describe('getLatestTableEventId (memory buffer)', () => {
}
})
})

describe('signalTableViewsChanged', () => {
beforeEach(() => {
vi.clearAllMocks()
})

it('appends a single views event carrying the tableId', async () => {
const tableId = uniqueTableId()
// The memory-buffer append is synchronous, so the fire-and-forget signal is
// observable immediately without awaiting the (unreturned) append promise.
signalTableViewsChanged(tableId)

const result = await readTableEventsSince(tableId, 0)
expect(result.status).toBe('ok')
if (result.status === 'ok') {
expect(result.events).toHaveLength(1)
expect(result.events[0].event).toEqual({ kind: 'views', tableId })
}
})
})
104 changes: 102 additions & 2 deletions apps/sim/lib/table/views/service.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import { tableViews } from '@sim/db/schema'
import { dbChainMockFns, queueTableRows, resetDbChainMock } from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { ColumnDefinition, TableViewConfig } from '@/lib/table/types'
import { normalizeStoredViewConfig, pruneViewConfig } from '@/lib/table/views/service'

const { mockSignalTableViewsChanged } = vi.hoisted(() => ({
mockSignalTableViewsChanged: vi.fn(),
}))
vi.mock('@/lib/table/events', () => ({
signalTableViewsChanged: mockSignalTableViewsChanged,
}))

import {
createTableView,
deleteTableView,
normalizeStoredViewConfig,
pruneViewConfig,
updateTableView,
} from '@/lib/table/views/service'

const columns: ColumnDefinition[] = [
{ id: 'col_a', name: 'Name', type: 'text' },
Expand Down Expand Up @@ -84,3 +100,87 @@ describe('normalizeStoredViewConfig', () => {
expect(out.filter).toBeNull()
})
})

describe('table-view mutations signal collaborators', () => {
const columns: ColumnDefinition[] = []
const viewRow = {
id: 'view-1',
tableId: 'table-1',
workspaceId: 'ws-1',
name: 'My View',
config: {},
isDefault: false,
createdBy: 'user-1',
createdAt: new Date('2026-01-01T00:00:00.000Z'),
updatedAt: new Date('2026-01-01T00:00:00.000Z'),
}

beforeEach(() => {
vi.clearAllMocks()
resetDbChainMock()
})

it('createTableView signals the table after inserting', async () => {
dbChainMockFns.returning.mockResolvedValueOnce([viewRow])

await createTableView({
tableId: 'table-1',
workspaceId: 'ws-1',
name: 'My View',
config: {},
userId: 'user-1',
columns,
})

expect(mockSignalTableViewsChanged).toHaveBeenCalledTimes(1)
expect(mockSignalTableViewsChanged).toHaveBeenCalledWith('table-1')
})

it('updateTableView signals when the target view exists', async () => {
queueTableRows(tableViews, [{ id: 'view-1' }]) // the in-transaction existence pre-check
dbChainMockFns.returning.mockResolvedValueOnce([viewRow]) // the update returning

const result = await updateTableView({
viewId: 'view-1',
tableId: 'table-1',
name: 'Renamed',
columns,
})

expect(result).not.toBeNull()
expect(mockSignalTableViewsChanged).toHaveBeenCalledTimes(1)
expect(mockSignalTableViewsChanged).toHaveBeenCalledWith('table-1')
})

it('updateTableView does NOT signal a no-op update on a missing view', async () => {
// No queued existence row → the pre-check finds nothing → returns null before any write.
const result = await updateTableView({
viewId: 'missing',
tableId: 'table-1',
name: 'Renamed',
columns,
})

expect(result).toBeNull()
expect(mockSignalTableViewsChanged).not.toHaveBeenCalled()
})

it('deleteTableView signals when a row was actually deleted', async () => {
dbChainMockFns.returning.mockResolvedValueOnce([{ id: 'view-1' }])

const deleted = await deleteTableView('view-1', 'table-1')

expect(deleted).toBe(true)
expect(mockSignalTableViewsChanged).toHaveBeenCalledTimes(1)
expect(mockSignalTableViewsChanged).toHaveBeenCalledWith('table-1')
})

it('deleteTableView does NOT signal when nothing was deleted', async () => {
dbChainMockFns.returning.mockResolvedValueOnce([])

const deleted = await deleteTableView('missing', 'table-1')

expect(deleted).toBe(false)
expect(mockSignalTableViewsChanged).not.toHaveBeenCalled()
})
})
11 changes: 11 additions & 0 deletions packages/testing/src/mocks/schema.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1136,6 +1136,17 @@ export const schemaMock = {
updatedAt: 'updatedAt',
completedAt: 'completedAt',
},
tableViews: {
id: 'id',
tableId: 'tableId',
workspaceId: 'workspaceId',
name: 'name',
config: 'config',
isDefault: 'isDefault',
createdBy: 'createdBy',
createdAt: 'createdAt',
updatedAt: 'updatedAt',
},
tableRowExecutions: {
tableId: 'tableId',
rowId: 'rowId',
Expand Down
Loading