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:
+22
-11
@@ -1,4 +1,4 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { QUERY_MAX_RECORDS_FROM_RELATION } from 'twenty-shared/constants';
|
||||
import { type ObjectRecordEvent } from 'twenty-shared/database-events';
|
||||
@@ -50,6 +50,8 @@ import { parseEventNameOrThrow } from 'src/engine/workspace-event-emitter/utils/
|
||||
|
||||
@Injectable()
|
||||
export class ObjectRecordEventPublisher {
|
||||
private readonly logger = new Logger(ObjectRecordEventPublisher.name);
|
||||
|
||||
constructor(
|
||||
private readonly subscriptionService: SubscriptionService,
|
||||
private readonly eventStreamService: EventStreamService,
|
||||
@@ -219,16 +221,25 @@ export class ObjectRecordEventPublisher {
|
||||
}
|
||||
|
||||
if (matchedEvents.length > 0) {
|
||||
await this.enrichEventBatchWithNestedRelations({
|
||||
objectMetadata: workspaceEventBatch.objectMetadata,
|
||||
events: matchedEvents.map(
|
||||
(matchedEvent) => matchedEvent.objectRecordEvent,
|
||||
),
|
||||
streamData,
|
||||
permissionsContext,
|
||||
workspaceId: workspaceEventBatch.workspaceId,
|
||||
roleId,
|
||||
});
|
||||
try {
|
||||
await this.enrichEventBatchWithNestedRelations({
|
||||
objectMetadata: workspaceEventBatch.objectMetadata,
|
||||
events: matchedEvents.map(
|
||||
(matchedEvent) => matchedEvent.objectRecordEvent,
|
||||
),
|
||||
streamData,
|
||||
permissionsContext,
|
||||
workspaceId: workspaceEventBatch.workspaceId,
|
||||
roleId,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`Failed to enrich nested relations for ${workspaceEventBatch.name} subscription event, broadcasting without them: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
);
|
||||
}
|
||||
|
||||
const payload: EventStreamPayload = {
|
||||
objectRecordEventsWithQueryIds: matchedEvents,
|
||||
|
||||
Reference in New Issue
Block a user