Skip to content

Commit 4bc6a17

Browse files
authored
v0.6.64: table limits env vars, workspace files improvements, integration blocks/connectors updates
2 parents d445b9c + 68f66ba commit 4bc6a17

219 files changed

Lines changed: 30375 additions & 3335 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/docs/content/docs/en/tools/stt.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ Transcribe audio and video files to text using leading AI providers. Supports mu
120120
| --------- | ---- | -------- | ----------- |
121121
| `provider` | string | Yes | STT provider \(elevenlabs\) |
122122
| `apiKey` | string | Yes | ElevenLabs API key |
123-
| `model` | string | No | ElevenLabs model to use \(scribe_v1, scribe_v1_experimental\) |
123+
| `model` | string | No | ElevenLabs model to use \(scribe_v2\) |
124124
| `audioFile` | file | No | Audio or video file to transcribe \(e.g., MP3, WAV, M4A, WEBM\) |
125125
| `audioFileReference` | file | No | Reference to audio/video file from previous blocks |
126126
| `audioUrl` | string | No | URL to audio or video file |

apps/realtime/src/handlers/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { setupConnectionHandlers } from '@/handlers/connection'
22
import { setupOperationsHandlers } from '@/handlers/operations'
33
import { setupPresenceHandlers } from '@/handlers/presence'
44
import { setupSubblocksHandlers } from '@/handlers/subblocks'
5+
import { setupTableHandlers } from '@/handlers/tables'
56
import { setupVariablesHandlers } from '@/handlers/variables'
67
import { setupWorkflowHandlers } from '@/handlers/workflow'
78
import type { AuthenticatedSocket } from '@/middleware/auth'
@@ -13,5 +14,6 @@ export function setupAllHandlers(socket: AuthenticatedSocket, roomManager: IRoom
1314
setupSubblocksHandlers(socket, roomManager)
1415
setupVariablesHandlers(socket, roomManager)
1516
setupPresenceHandlers(socket, roomManager)
17+
setupTableHandlers(socket, roomManager)
1618
setupConnectionHandlers(socket, roomManager)
1719
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { createLogger } from '@sim/logger'
2+
import type { AuthenticatedSocket } from '@/middleware/auth'
3+
import { verifyTableAccess } from '@/middleware/permissions'
4+
import { type IRoomManager, tableRoomName } from '@/rooms/types'
5+
6+
const logger = createLogger('TableHandlers')
7+
8+
/**
9+
* Wires `join-table` / `leave-table` socket events. Tables don't track presence
10+
* or last-modified state — joining is a thin wrapper around `socket.join` so the
11+
* Sim API → Realtime HTTP bridge can broadcast row updates back to subscribed clients.
12+
*/
13+
export function setupTableHandlers(socket: AuthenticatedSocket, _roomManager: IRoomManager) {
14+
socket.on('join-table', async ({ tableId }: { tableId?: string }) => {
15+
try {
16+
if (!tableId || typeof tableId !== 'string') {
17+
socket.emit('join-table-error', {
18+
tableId: tableId ?? null,
19+
error: 'tableId required',
20+
code: 'INVALID_TABLE_ID',
21+
retryable: false,
22+
})
23+
return
24+
}
25+
26+
const userId = socket.userId
27+
if (!userId) {
28+
socket.emit('join-table-error', {
29+
tableId,
30+
error: 'Authentication required',
31+
code: 'AUTHENTICATION_REQUIRED',
32+
retryable: false,
33+
})
34+
return
35+
}
36+
37+
const { hasAccess } = await verifyTableAccess(userId, tableId)
38+
if (!hasAccess) {
39+
socket.emit('join-table-error', {
40+
tableId,
41+
error: 'Access denied to table',
42+
code: 'ACCESS_DENIED',
43+
retryable: false,
44+
})
45+
return
46+
}
47+
48+
const room = tableRoomName(tableId)
49+
socket.join(room)
50+
socket.emit('join-table-success', { tableId, socketId: socket.id })
51+
logger.debug(`Socket ${socket.id} (user ${userId}) joined ${room}`)
52+
} catch (error) {
53+
logger.error(`Error joining table room:`, error)
54+
socket.emit('join-table-error', {
55+
tableId: null,
56+
error: 'Failed to join table',
57+
code: 'JOIN_TABLE_FAILED',
58+
retryable: true,
59+
})
60+
}
61+
})
62+
63+
socket.on('leave-table', async ({ tableId }: { tableId?: string }) => {
64+
try {
65+
if (!tableId || typeof tableId !== 'string') return
66+
const room = tableRoomName(tableId)
67+
socket.leave(room)
68+
logger.debug(`Socket ${socket.id} left ${room}`)
69+
} catch (error) {
70+
logger.error(`Error leaving table room:`, error)
71+
}
72+
})
73+
}

apps/realtime/src/middleware/permissions.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,51 @@ export async function verifyWorkflowAccess(
131131
return { hasAccess: false }
132132
}
133133
}
134+
135+
/**
136+
* Verify a user has read access to a table by virtue of workspace permission.
137+
* Mirrors `verifyWorkflowAccess` for the table-room socket join check.
138+
*/
139+
export async function verifyTableAccess(
140+
userId: string,
141+
tableId: string
142+
): Promise<{ hasAccess: boolean; workspaceId?: string }> {
143+
try {
144+
const { userTableDefinitions, permissions } = await import('@sim/db')
145+
const tableData = await db
146+
.select({ workspaceId: userTableDefinitions.workspaceId })
147+
.from(userTableDefinitions)
148+
.where(and(eq(userTableDefinitions.id, tableId), isNull(userTableDefinitions.archivedAt)))
149+
.limit(1)
150+
151+
if (!tableData.length) {
152+
logger.warn(`Table ${tableId} not found`)
153+
return { hasAccess: false }
154+
}
155+
const { workspaceId } = tableData[0]
156+
if (!workspaceId) return { hasAccess: false }
157+
158+
const [permissionRow] = await db
159+
.select({ permissionType: permissions.permissionType })
160+
.from(permissions)
161+
.where(
162+
and(
163+
eq(permissions.userId, userId),
164+
eq(permissions.entityType, 'workspace'),
165+
eq(permissions.entityId, workspaceId)
166+
)
167+
)
168+
.limit(1)
169+
170+
if (!permissionRow?.permissionType) {
171+
logger.warn(
172+
`User ${userId} has no permission for workspace ${workspaceId} (table ${tableId})`
173+
)
174+
return { hasAccess: false }
175+
}
176+
return { hasAccess: true, workspaceId }
177+
} catch (error) {
178+
logger.error(`Error verifying table access for user ${userId}, table ${tableId}:`, error)
179+
return { hasAccess: false }
180+
}
181+
}

apps/realtime/src/rooms/memory-manager.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import { createLogger } from '@sim/logger'
22
import type { Server } from 'socket.io'
3-
import type { IRoomManager, UserPresence, UserSession, WorkflowRoom } from '@/rooms/types'
3+
import {
4+
type IRoomManager,
5+
type TableRowUpdatedPayload,
6+
tableRoomName,
7+
type UserPresence,
8+
type UserSession,
9+
type WorkflowRoom,
10+
} from '@/rooms/types'
411

512
const logger = createLogger('MemoryRoomManager')
613

@@ -255,4 +262,23 @@ export class MemoryRoomManager implements IRoomManager {
255262

256263
logger.info(`Notified ${room.users.size} users about workflow deployment change: ${workflowId}`)
257264
}
265+
266+
emitToTable<T = unknown>(tableId: string, event: string, payload: T): void {
267+
this._io.to(tableRoomName(tableId)).emit(event, payload)
268+
}
269+
270+
async handleTableRowUpdated(tableId: string, payload: TableRowUpdatedPayload): Promise<void> {
271+
this.emitToTable(tableId, 'table-row-updated', { tableId, ...payload })
272+
}
273+
274+
async handleTableRowDeleted(tableId: string, rowId: string): Promise<void> {
275+
this.emitToTable(tableId, 'table-row-deleted', { tableId, rowId })
276+
}
277+
278+
async handleTableDeleted(tableId: string): Promise<void> {
279+
logger.info(`Handling table deletion notification for ${tableId}`)
280+
this.emitToTable(tableId, 'table-deleted', { tableId, timestamp: Date.now() })
281+
// Eject sockets so they don't hold a stale room. Cross-pod safe via socket.io.
282+
await this._io.in(tableRoomName(tableId)).socketsLeave(tableRoomName(tableId))
283+
}
258284
}

apps/realtime/src/rooms/redis-manager.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { createLogger } from '@sim/logger'
22
import { createClient, type RedisClientType } from 'redis'
33
import type { Server } from 'socket.io'
4-
import type { IRoomManager, UserPresence, UserSession } from '@/rooms/types'
4+
import {
5+
type IRoomManager,
6+
type TableRowUpdatedPayload,
7+
tableRoomName,
8+
type UserPresence,
9+
type UserSession,
10+
} from '@/rooms/types'
511

612
const logger = createLogger('RedisRoomManager')
713

@@ -457,4 +463,23 @@ export class RedisRoomManager implements IRoomManager {
457463
const userCount = await this.getUniqueUserCount(workflowId)
458464
logger.info(`Notified ${userCount} users about workflow deployment change: ${workflowId}`)
459465
}
466+
467+
emitToTable<T = unknown>(tableId: string, event: string, payload: T): void {
468+
this._io.to(tableRoomName(tableId)).emit(event, payload)
469+
}
470+
471+
async handleTableRowUpdated(tableId: string, payload: TableRowUpdatedPayload): Promise<void> {
472+
this.emitToTable(tableId, 'table-row-updated', { tableId, ...payload })
473+
}
474+
475+
async handleTableRowDeleted(tableId: string, rowId: string): Promise<void> {
476+
this.emitToTable(tableId, 'table-row-deleted', { tableId, rowId })
477+
}
478+
479+
async handleTableDeleted(tableId: string): Promise<void> {
480+
logger.info(`Handling table deletion notification for ${tableId}`)
481+
this.emitToTable(tableId, 'table-deleted', { tableId, timestamp: Date.now() })
482+
// Eject sockets across all pods via socket.io's Redis adapter.
483+
await this._io.in(tableRoomName(tableId)).socketsLeave(tableRoomName(tableId))
484+
}
460485
}

apps/realtime/src/rooms/types.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,4 +143,45 @@ export interface IRoomManager {
143143
* Handle workflow deployment change - notify users to refresh deployment state
144144
*/
145145
handleWorkflowDeployed(workflowId: string): Promise<void>
146+
147+
/**
148+
* Emit an event to all clients in a table room (`table:${tableId}`).
149+
* Tables don't track presence/last-modified state — just pub/sub.
150+
*/
151+
emitToTable<T = unknown>(tableId: string, event: string, payload: T): void
152+
153+
/**
154+
* Notify all clients in a table room of a row write (insert/update/cell-state-change).
155+
* Sim API calls this via the `/api/table-row-updated` HTTP bridge after every successful
156+
* row commit; the client merges the delta into its React Query cache.
157+
*/
158+
handleTableRowUpdated(tableId: string, payload: TableRowUpdatedPayload): Promise<void>
159+
160+
/**
161+
* Notify all clients in a table room that a row has been deleted.
162+
*/
163+
handleTableRowDeleted(tableId: string, rowId: string): Promise<void>
164+
165+
/**
166+
* Notify all clients in a table room that the table has been deleted; eject sockets.
167+
*/
168+
handleTableDeleted(tableId: string): Promise<void>
169+
}
170+
171+
/**
172+
* Payload broadcast on `table-row-updated`. Mirrors the shape of `TableRow.data` so
173+
* the client can merge directly into its React Query rows cache. `position` and
174+
* `updatedAt` are included for cache reconciliation; `data` is the full row data
175+
* (not a per-cell delta) — see plan Notes.
176+
*/
177+
export interface TableRowUpdatedPayload {
178+
rowId: string
179+
data: Record<string, unknown>
180+
/** Per-workflow-group execution state. Keyed by `WorkflowGroup.id`. */
181+
executions?: Record<string, unknown>
182+
position: number
183+
updatedAt: string | number
146184
}
185+
186+
/** Socket.IO room name for a table. Namespaced from workflow rooms. */
187+
export const tableRoomName = (tableId: string): string => `table:${tableId}`

apps/realtime/src/routes/http.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,52 @@ export function createHttpHandler(roomManager: IRoomManager, logger: Logger) {
150150
return
151151
}
152152

153+
// Handle table row write notifications from the Sim API
154+
if (req.method === 'POST' && req.url === '/api/table-row-updated') {
155+
try {
156+
const body = await readRequestBody(req)
157+
const { tableId, rowId, data, executions, position, updatedAt } = JSON.parse(body)
158+
await roomManager.handleTableRowUpdated(tableId, {
159+
rowId,
160+
data,
161+
executions,
162+
position,
163+
updatedAt,
164+
})
165+
sendSuccess(res)
166+
} catch (error) {
167+
logger.error('Error handling table row update notification:', error)
168+
sendError(res, 'Failed to process table row update')
169+
}
170+
return
171+
}
172+
173+
if (req.method === 'POST' && req.url === '/api/table-row-deleted') {
174+
try {
175+
const body = await readRequestBody(req)
176+
const { tableId, rowId } = JSON.parse(body)
177+
await roomManager.handleTableRowDeleted(tableId, rowId)
178+
sendSuccess(res)
179+
} catch (error) {
180+
logger.error('Error handling table row deletion notification:', error)
181+
sendError(res, 'Failed to process table row deletion')
182+
}
183+
return
184+
}
185+
186+
if (req.method === 'POST' && req.url === '/api/table-deleted') {
187+
try {
188+
const body = await readRequestBody(req)
189+
const { tableId } = JSON.parse(body)
190+
await roomManager.handleTableDeleted(tableId)
191+
sendSuccess(res)
192+
} catch (error) {
193+
logger.error('Error handling table deletion notification:', error)
194+
sendError(res, 'Failed to process table deletion')
195+
}
196+
return
197+
}
198+
153199
res.writeHead(404, { 'Content-Type': 'application/json' })
154200
res.end(JSON.stringify({ error: 'Not found' }))
155201
}

0 commit comments

Comments
 (0)