fix: broadcast timeline activities to live SSE subscriptions (#21104)

## Context
Timeline activities never updated in real time. They were explicitly
excluded from the database-event pipeline
(formatTwentyOrmEventToDatabaseBatchEvent early-returned for the
timelineActivity object), so no SSE event was ever broadcast, and the
frontend timeline only refreshed on mount/manual refetch.

## Implementation
Backend
- Feat: Stop dropping timeline-activity events in
formatTwentyOrmEventToDatabaseBatchEvent.
Instead route them through EntityEventsToDbListener, which publishes
them directly to live subscriptions (but still skipping webhook/audit
handling).
- Fix: Harden ObjectRecordEventPublisher: wrap nested-relation
enrichment in try/catch so a failure broadcasts the event without
relations instead of dropping it (logs a warning).
- Fix: Skip unreadable relation targets in CommonSelectFieldsHelper when
the role lacks canReadObjectRecords, preventing errors while computing
selected fields.
- Fix: Support MORPH_RELATION alongside RELATION in RLS row-level
permission predicate matching (timeline activities use morph targets).

Frontend
- Feat: useTimelineActivities now registers the timeline query with the
SSE system via useListenToEventsForQuery and refetches on incoming
timeline-activity record operations.
- Feat: Add a skip option to useListenToEventsForQuery so the listener
isn't registered when the object has no timeline field.

## Test


https://github.com/user-attachments/assets/ed1d1c66-d6ea-434d-ac9c-9b83d2b78338


Note: "UpdatedBy" seems to be listen to and visible in the timeline
activity summary, this is probably a bug that we want to fix
This commit is contained in:
Weiko
2026-06-01 16:32:08 +02:00
committed by GitHub
parent 381ca32055
commit b9e5ff2065
7 changed files with 98 additions and 28 deletions
@@ -7,7 +7,6 @@ import {
ObjectRecordUpsertEvent,
type ObjectRecordDiff,
} from 'twenty-shared/database-events';
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import {
assertUnreachable,
isDefined,
@@ -47,13 +46,6 @@ export const formatTwentyOrmEventToDatabaseBatchEvent = <
recordsAfter?: T[];
recordsBefore?: T[];
}): DatabaseBatchEventInput<T, DatabaseEventAction> | undefined => {
if (
objectMetadataItem.universalIdentifier ===
STANDARD_OBJECTS.timelineActivity.universalIdentifier
) {
return;
}
const objectMetadataNameSingular = objectMetadataItem.nameSingular;
let events: (
@@ -208,7 +208,8 @@ export const isRecordMatchingRLSRowLevelPermissionPredicate = ({
objectFields.find((field) => field.name === filterKey) ??
objectFields.find(
(field) =>
field.type === FieldMetadataType.RELATION &&
(field.type === FieldMetadataType.RELATION ||
field.type === FieldMetadataType.MORPH_RELATION) &&
computeMorphOrRelationFieldJoinColumnName({ name: field.name }) ===
filterKey,
);
@@ -411,7 +412,8 @@ export const isRecordMatchingRLSRowLevelPermissionPredicate = ({
});
});
}
case FieldMetadataType.RELATION: {
case FieldMetadataType.RELATION:
case FieldMetadataType.MORPH_RELATION: {
const isJoinColumn =
computeMorphOrRelationFieldJoinColumnName({
name: objectMetadataField.name,