feat(tables): trigger workflows on row deletes · simstudioai/sim@38027ad · GitHub
Skip to content

Commit 38027ad

Browse files
committed
feat(tables): trigger workflows on row deletes
1 parent 8825f1b commit 38027ad

11 files changed

Lines changed: 313 additions & 105 deletions

File tree

‎apps/sim/background/cleanup-table-row-ttl.test.ts‎

Lines changed: 42 additions & 14 deletions

‎apps/sim/background/cleanup-table-row-ttl.ts‎

Lines changed: 57 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ import { getColumnId } from '@/lib/table/column-keys'
88
import { signalTableRowsChanged } from '@/lib/table/events'
99
import { assertRowDelete, TableLockedError } from '@/lib/table/mutation-locks'
1010
import type { DbTransaction } from '@/lib/table/planner'
11+
import type { DeletedTableRow } from '@/lib/table/rows/ordering'
1112
import { withLockedTable } from '@/lib/table/service'
13+
import { fireTableTrigger } from '@/lib/table/trigger'
1214
import { isTableRowTtlEnabled } from '@/lib/table/ttl-availability'
15+
import type { RowData, TableSchema } from '@/lib/table/types'
1316

1417
const logger = createLogger('CleanupTableRowTtl')
1518
const cleanupDb = dbFor('cleanup')
@@ -23,12 +26,20 @@ interface ExpiredTtlTableRef {
2326
workspaceId: string
2427
}
2528

26-
interface DeletedTtlBatch {
27-
attempted: boolean
29+
interface DeletedTtlRows {
2830
deleted: number
2931
cursor: TtlCleanupCursor | null
32+
rows: DeletedTableRow[]
3033
}
3134

35+
type DeletedTtlBatch =
36+
| { attempted: false; deleted: 0; cursor: null }
37+
| (DeletedTtlRows & {
38+
attempted: true
39+
tableName: string
40+
schema: TableSchema
41+
})
42+
3243
interface TtlCleanupCursor {
3344
createdAt: string
3445
id: string
@@ -85,34 +96,30 @@ async function listExpiredTtlTables(nowEpochSeconds: number): Promise<ExpiredTtl
8596
return Array.isArray(rows) ? rows : []
8697
}
8798

88-
function parseDeletedBatch(rows: unknown): Omit<DeletedTtlBatch, 'attempted'> {
89-
const [row] = Array.isArray(rows)
90-
? (rows as Array<{
91-
count?: number | string
92-
createdAt?: string | null
93-
lastId?: string | null
94-
}>)
95-
: []
96-
if (!row) throw new Error('Table row TTL cleanup did not return a deleted count')
97-
98-
const deleted = Number(row.count)
99-
if (!Number.isSafeInteger(deleted) || deleted < 0 || deleted > TTL_CLEANUP_BATCH_SIZE) {
99+
function parseDeletedBatch(rows: unknown): DeletedTtlRows {
100+
if (!Array.isArray(rows)) {
101+
throw new Error('Table row TTL cleanup did not return deleted rows')
102+
}
103+
const deletedRows = rows as Array<{ id?: unknown; data?: unknown; createdAt?: unknown }>
104+
if (deletedRows.length > TTL_CLEANUP_BATCH_SIZE) {
100105
throw new Error('Table row TTL cleanup returned an invalid deleted count')
101106
}
102-
if (deleted > 0) {
103-
if (typeof row.lastId !== 'string') {
107+
const parsed = deletedRows.map((row) => {
108+
if (typeof row.id !== 'string') {
104109
throw new Error('Table row TTL cleanup did not return a row cursor')
105110
}
106111
if (typeof row.createdAt !== 'string') {
107112
throw new Error('Table row TTL cleanup did not return a creation-time cursor')
108113
}
109-
}
114+
return {
115+
cursor: { createdAt: row.createdAt, id: row.id },
116+
row: { id: row.id, data: row.data as RowData },
117+
}
118+
})
110119
return {
111-
deleted,
112-
cursor:
113-
typeof row.createdAt === 'string' && typeof row.lastId === 'string'
114-
? { createdAt: row.createdAt, id: row.lastId }
115-
: null,
120+
deleted: parsed.length,
121+
cursor: parsed[parsed.length - 1]?.cursor ?? null,
122+
rows: parsed.map(({ row }) => row),
116123
}
117124
}
118125

@@ -123,12 +130,8 @@ async function deleteExpiredTableRowBatch(
123130
columnKey: string,
124131
nowEpochSeconds: number,
125132
after?: TtlCleanupCursor
126-
): Promise<Omit<DeletedTtlBatch, 'attempted'>> {
127-
const rows = await trx.execute<{
128-
count: number | string
129-
createdAt: string | null
130-
lastId: string | null
131-
}>(sql`
133+
): Promise<DeletedTtlRows> {
134+
const rows = await trx.execute<{ id: string; data: RowData; createdAt: string }>(sql`
132135
WITH candidates AS MATERIALIZED (
133136
SELECT table_row.id
134137
FROM ${userTableRows} AS table_row
@@ -148,18 +151,14 @@ async function deleteExpiredTableRowBatch(
148151
DELETE FROM ${userTableRows} AS table_row
149152
USING candidates
150153
WHERE table_row.id = candidates.id
151-
RETURNING table_row.id, table_row.created_at
154+
RETURNING
155+
table_row.id,
156+
table_row.data,
157+
to_char(table_row.created_at, 'YYYY-MM-DD"T"HH24:MI:SS.US') AS "createdAt"
152158
)
153-
SELECT
154-
count(*)::integer AS count,
155-
(array_agg(id ORDER BY created_at DESC, id DESC))[1] AS "lastId",
156-
(
157-
array_agg(
158-
to_char(created_at, 'YYYY-MM-DD"T"HH24:MI:SS.US')
159-
ORDER BY created_at DESC, id DESC
160-
)
161-
)[1] AS "createdAt"
159+
SELECT id, data, "createdAt"
162160
FROM deleted
161+
ORDER BY "createdAt", id
163162
`)
164163
return parseDeletedBatch(rows)
165164
}
@@ -170,9 +169,9 @@ async function deleteExpiredRowsForTable(
170169
after?: TtlCleanupCursor
171170
): Promise<DeletedTtlBatch> {
172171
try {
173-
return await withLockedTable(
172+
const batch = await withLockedTable(
174173
ref.id,
175-
async (table, trx) => {
174+
async (table, trx): Promise<DeletedTtlBatch> => {
176175
try {
177176
assertRowDelete(table)
178177
} catch (error) {
@@ -193,10 +192,27 @@ async function deleteExpiredRowsForTable(
193192
nowEpochSeconds,
194193
after
195194
)
196-
return { attempted: true, ...batch }
195+
return {
196+
attempted: true,
197+
...batch,
198+
tableName: table.name,
199+
schema: table.schema,
200+
} satisfies DeletedTtlBatch
197201
},
198202
{ expectedWorkspaceId: ref.workspaceId }
199203
)
204+
if (batch.attempted && batch.rows.length > 0) {
205+
void fireTableTrigger(
206+
ref.id,
207+
batch.tableName,
208+
'delete',
209+
batch.rows,
210+
null,
211+
batch.schema,
212+
'ttl-cleanup'
213+
)
214+
}
215+
return batch
200216
} catch (error) {
201217
if (asOrchestrationError(error)?.code === 'not_found') {
202218
return { attempted: false, deleted: 0, cursor: null }

‎apps/sim/lib/table/__tests__/service-filter-threading.test.ts‎

Lines changed: 54 additions & 1 deletion

0 commit comments

Comments
 (0)