Skip to content

Add delete and restore event handling for table and board#17489

Merged
lucasbordeau merged 2 commits intomainfrom
feat/add-delete-restore-sse-event-front-handling
Jan 27, 2026
Merged

Add delete and restore event handling for table and board#17489
lucasbordeau merged 2 commits intomainfrom
feat/add-delete-restore-sse-event-front-handling

Conversation

@lucasbordeau
Copy link
Copy Markdown
Contributor

@lucasbordeau lucasbordeau commented Jan 27, 2026

This PR adds what is required to handle soft-delete and restore SSE events in virtualized table and board.

Restore is not handled in board for now as it requires respecting sorts when inserting record ids.

Since virtualized table is refetching small chunks, we just refetch for now.

The long term goal is to handle event handling without refetching in all main components, and also handle SSE events that have the same origin that the current tab. But for now we implement what is easily doable.

QA

Delete between table and board (delete only) :

Enregistrement.de.l.ecran.2026-01-27.a.17.57.38.mov

Delete and restore between table and table :

Enregistrement.de.l.ecran.2026-01-27.a.17.58.39.mov

Copilot AI review requested due to automatic review settings January 27, 2026 17:03
@lucasbordeau lucasbordeau requested review from thomtrp and removed request for Copilot January 27, 2026 17:05
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Jan 27, 2026

Greptile Overview

Greptile Summary

This PR implements SSE event handling for soft-delete and restore operations in both table and board views.

Key Changes:

  • Added new hooks useTriggerOptimisticEffectFromSseDeleteEvents and useTriggerOptimisticEffectFromSseRestoreEvents to handle cache updates when records are deleted or restored via SSE events
  • Extended ObjectRecordOperation type to include deletedRecordId(s) and restoredRecord(s) data
  • Updated board view to remove deleted records from their groups via new useRemoveRecordsFromBoard hook
  • Configured table virtualization to refetch data on delete/restore events (debounced 50ms to avoid race conditions with own events)
  • Enhanced server-side RLS filtering to correctly match deleted/restored records by adding shouldIgnoreSoftDeleteDefaultFilter parameter
  • Updated delete/restore mutation hooks to dispatch browser events with record IDs/data for proper event propagation

Limitations:

  • Restore operations in board view are not yet implemented, as noted in PR description (requires respecting sort order when inserting records)
  • Table view uses refetch approach rather than optimistic insertion/removal due to virtualized chunks

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The implementation is well-structured and follows established patterns in the codebase. The changes are incremental and pragmatic, handling delete events in board view while using refetch for table view virtualization. The server-side RLS filtering enhancement properly handles soft-deleted records. Code follows naming conventions and import organization standards. The PR appropriately documents limitations (restore in board view) rather than implementing incomplete solutions.
  • No files require special attention

Important Files Changed

Filename Overview
packages/twenty-front/src/modules/sse-db-event/hooks/useTriggerOptimisticEffectFromSseDeleteEvents.ts New hook to handle SSE delete events by updating cache with deletedAt timestamp and triggering optimistic updates
packages/twenty-front/src/modules/sse-db-event/hooks/useTriggerOptimisticEffectFromSseRestoreEvents.ts New hook to handle SSE restore events by updating cache and triggering optimistic updates for restored records
packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardDataChangedEffect.tsx Added delete-one/delete-many handlers to remove records from board, restore operations currently no-op as noted in PR description
packages/twenty-front/src/modules/object-record/record-board/hooks/useRemoveRecordsFromBoard.ts New hook to remove record IDs from board groups by filtering them out from recordIndexRecordIdsByGroup state
packages/twenty-server/src/engine/workspace-event-emitter/workspace-event-emitter.service.ts Updated to pass shouldIgnoreSoftDeleteDefaultFilter flag for delete/restore events to correctly match records in subscriptions

Sequence Diagram

sequenceDiagram
    participant Server as Twenty Server
    participant SSE as SSE Connection
    participant EventHandler as useTriggerOptimisticEffectFromSseEvents
    participant DeleteHandler as useTriggerOptimisticEffectFromSseDeleteEvents
    participant RestoreHandler as useTriggerOptimisticEffectFromSseRestoreEvents
    participant Cache as Apollo Cache
    participant BoardEffect as RecordBoardDataChangedEffect
    participant TableEffect as RecordTableVirtualizedDataChangedEffect
    participant Board as Board UI
    participant Table as Table UI

    Note over Server,Table: Delete Operation Flow
    Server->>SSE: Emit DELETED event (before/after record state)
    SSE->>EventHandler: objectRecordEvents[]
    EventHandler->>EventHandler: Group by object type and event type
    EventHandler->>DeleteHandler: DELETED events for object
    DeleteHandler->>Cache: updateRecordFromCache (set deletedAt)
    DeleteHandler->>Cache: triggerUpdateRecordOptimisticEffectByBatch
    DeleteHandler->>Cache: refetchAggregateQueries (debounced 100ms)
    DeleteHandler-->>EventHandler: Done
    EventHandler->>EventHandler: dispatchObjectRecordOperationBrowserEvent
    EventHandler->>BoardEffect: delete-one/delete-many operation
    BoardEffect->>Board: removeRecordsFromBoard (filter IDs from groups)
    EventHandler->>TableEffect: delete-one/delete-many operation
    TableEffect->>Table: resetVirtualization (debounced 50ms, refetch)

    Note over Server,Table: Restore Operation Flow
    Server->>SSE: Emit RESTORED event (before/after record state)
    SSE->>EventHandler: objectRecordEvents[]
    EventHandler->>EventHandler: Group by object type and event type
    EventHandler->>RestoreHandler: RESTORED events for object
    RestoreHandler->>Cache: updateRecordFromCache (clear deletedAt)
    RestoreHandler->>Cache: triggerUpdateRecordOptimisticEffectByBatch
    RestoreHandler->>Cache: refetchAggregateQueries (debounced 100ms)
    RestoreHandler-->>EventHandler: Done
    EventHandler->>EventHandler: dispatchObjectRecordOperationBrowserEvent
    EventHandler->>BoardEffect: restore-one/restore-many operation
    BoardEffect->>BoardEffect: No-op (not yet implemented)
    EventHandler->>TableEffect: restore-one/restore-many operation
    TableEffect->>Table: resetVirtualization (debounced 50ms, refetch)

    Note over Server,Cache: Server-Side RLS Filtering
    Server->>Server: shouldIgnoreSoftDeleteDefaultFilter=true for DELETED/RESTORED
    Server->>Server: isRecordMatchingRLSRowLevelPermissionPredicate
    Server->>SSE: Only emit to subscribers with matching permissions
Loading

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No files reviewed, no comments

Edit Code Review Agent Settings | Greptile

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 15 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/twenty-server/src/engine/twenty-orm/utils/is-record-matching-rls-row-level-permission-predicate.util.ts">

<violation number="1" location="packages/twenty-server/src/engine/twenty-orm/utils/is-record-matching-rls-row-level-permission-predicate.util.ts:183">
P1: Leaf filters that explicitly target `deletedAt` can no longer match soft-deleted rows. The new guard rejects any leaf filter when `record.deletedAt` is defined, so queries like `{ deletedAt: { is: 'NOT_NULL' } }` always fail. Preserve the previous behavior by only rejecting when the filter doesn’t specify `deletedAt`.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment on lines +183 to +186
const shouldRejectMatchingBecauseRecordIsSoftDeleted =
isLeafFilter(filter) &&
shouldTakeDeletedAtIntoAccount &&
isDefined(record.deletedAt);
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Leaf filters that explicitly target deletedAt can no longer match soft-deleted rows. The new guard rejects any leaf filter when record.deletedAt is defined, so queries like { deletedAt: { is: 'NOT_NULL' } } always fail. Preserve the previous behavior by only rejecting when the filter doesn’t specify deletedAt.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/twenty-server/src/engine/twenty-orm/utils/is-record-matching-rls-row-level-permission-predicate.util.ts, line 183:

<comment>Leaf filters that explicitly target `deletedAt` can no longer match soft-deleted rows. The new guard rejects any leaf filter when `record.deletedAt` is defined, so queries like `{ deletedAt: { is: 'NOT_NULL' } }` always fail. Preserve the previous behavior by only rejecting when the filter doesn’t specify `deletedAt`.</comment>

<file context>
@@ -166,14 +172,21 @@ export const isRecordMatchingRLSRowLevelPermissionPredicate = ({
+  const shouldTakeDeletedAtIntoAccount =
+    shouldIgnoreSoftDeleteDefaultFilter !== true;
+
+  const shouldRejectMatchingBecauseRecordIsSoftDeleted =
+    isLeafFilter(filter) &&
+    shouldTakeDeletedAtIntoAccount &&
</file context>
Suggested change
const shouldRejectMatchingBecauseRecordIsSoftDeleted =
isLeafFilter(filter) &&
shouldTakeDeletedAtIntoAccount &&
isDefined(record.deletedAt);
const shouldRejectMatchingBecauseRecordIsSoftDeleted =
isLeafFilter(filter) &&
shouldTakeDeletedAtIntoAccount &&
isDefined(record.deletedAt) &&
filter.deletedAt === undefined;
Fix with Cubic

@github-actions
Copy link
Copy Markdown
Contributor

🚀 Preview Environment Ready!

Your preview environment is available at: http://bore.pub:3471

This environment will automatically shut down when the PR is closed or after 5 hours.

Copy link
Copy Markdown
Contributor

@thomtrp thomtrp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@lucasbordeau lucasbordeau added this pull request to the merge queue Jan 27, 2026
Merged via the queue into main with commit 2ffe2d8 Jan 27, 2026
82 of 83 checks passed
@lucasbordeau lucasbordeau deleted the feat/add-delete-restore-sse-event-front-handling branch January 27, 2026 21:21
@twenty-eng-sync
Copy link
Copy Markdown

Hey @lucasbordeau! After you've done the QA of your Pull Request, you can mark it as done here. Thank you!

1 similar comment
@twenty-eng-sync
Copy link
Copy Markdown

Hey @lucasbordeau! After you've done the QA of your Pull Request, you can mark it as done here. Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants