Add SSE for metadata and plug front components (#17998)
Create the necessary tooling to listen to metadata events and plug it to the front components. Now we have a hot reload like experience when we edit a component in an app. ## Backend - Split `EventWithQueryIds` into `ObjectRecordEventWithQueryIds` and `MetadataEventWithQueryIds` - Publish metadata event batches to active SSE streams in `MetadataEventsToDbListener` ## Frontend - Create a metadata event dispatching pipeline: SSE metadata events are grouped by metadata name, transformed into `MetadataOperationBrowserEventDetail` objects, and dispatched as browser `CustomEvents` - Add `useListenToMetadataOperationBrowserEvent` hook for consuming metadata operation events filtered by metadata name and operation type - Rename `useListenToObjectRecordEventsForQuery` to `useListenToEventsForQuery`, now accepting both `RecordGqlOperationSignature` and `MetadataGqlOperationSignature` - Implement `useOnFrontComponentUpdated` which subscribes to front component metadata events and updates the Apollo cache when the component is modified - Add `builtComponentChecksum` to the front component query and appends it to the component URL for browser cache invalidation
This commit is contained in:
+3
-2
@@ -1,7 +1,8 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import GraphQLJSON from 'graphql-type-json';
|
||||
import { type RecordGqlOperationSignature } from 'twenty-shared/types';
|
||||
|
||||
import { type RecordOrMetadataGqlOperationSignature } from 'src/engine/subscriptions/types/event-stream-data.type';
|
||||
|
||||
@InputType()
|
||||
export class AddQuerySubscriptionInput {
|
||||
@@ -12,5 +13,5 @@ export class AddQuerySubscriptionInput {
|
||||
queryId: string;
|
||||
|
||||
@Field(() => GraphQLJSON)
|
||||
operationSignature: RecordGqlOperationSignature;
|
||||
operationSignature: RecordOrMetadataGqlOperationSignature;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { MetadataEventDTO } from './metadata-event.dto';
|
||||
import { ObjectRecordEventDTO } from './object-record-event.dto';
|
||||
|
||||
@ObjectType('EventWithQueryIds')
|
||||
export class EventWithQueryIdsDTO {
|
||||
@ObjectType('ObjectRecordEventWithQueryIds')
|
||||
export class ObjectRecordEventWithQueryIdsDTO {
|
||||
@Field(() => [String])
|
||||
queryIds: string[];
|
||||
|
||||
@Field(() => ObjectRecordEventDTO)
|
||||
event: ObjectRecordEventDTO;
|
||||
objectRecordEvent: ObjectRecordEventDTO;
|
||||
}
|
||||
|
||||
@ObjectType('MetadataEventWithQueryIds')
|
||||
export class MetadataEventWithQueryIdsDTO {
|
||||
@Field(() => [String])
|
||||
queryIds: string[];
|
||||
|
||||
@Field(() => MetadataEventDTO)
|
||||
metadataEvent: MetadataEventDTO;
|
||||
}
|
||||
|
||||
@ObjectType('EventSubscription')
|
||||
@@ -16,6 +26,9 @@ export class EventSubscriptionDTO {
|
||||
@Field(() => String)
|
||||
eventStreamId: string;
|
||||
|
||||
@Field(() => [EventWithQueryIdsDTO])
|
||||
eventWithQueryIdsList: EventWithQueryIdsDTO[];
|
||||
@Field(() => [ObjectRecordEventWithQueryIdsDTO])
|
||||
objectRecordEventsWithQueryIds: ObjectRecordEventWithQueryIdsDTO[];
|
||||
|
||||
@Field(() => [MetadataEventWithQueryIdsDTO])
|
||||
metadataEventsWithQueryIds: MetadataEventWithQueryIdsDTO[];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { ObjectRecordEventPropertiesDTO } from 'src/engine/subscriptions/dtos/object-record-event-properties.dto';
|
||||
import { MetadataEventAction } from 'src/engine/subscriptions/enums/metadata-event-action.enum';
|
||||
|
||||
@ObjectType('MetadataEvent')
|
||||
export class MetadataEventDTO {
|
||||
@Field(() => MetadataEventAction)
|
||||
type: MetadataEventAction;
|
||||
|
||||
@Field(() => String)
|
||||
metadataName: string;
|
||||
|
||||
@Field(() => String)
|
||||
recordId: string;
|
||||
|
||||
@Field(() => ObjectRecordEventPropertiesDTO)
|
||||
properties: ObjectRecordEventPropertiesDTO;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
export enum MetadataEventAction {
|
||||
CREATED = 'created',
|
||||
UPDATED = 'updated',
|
||||
DELETED = 'deleted',
|
||||
}
|
||||
|
||||
registerEnumType(MetadataEventAction, {
|
||||
name: 'MetadataEventAction',
|
||||
description: 'Metadata Event Action',
|
||||
});
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
|
||||
import { type RecordGqlOperationSignature } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type SerializableAuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
@@ -15,7 +14,10 @@ import {
|
||||
EventStreamException,
|
||||
EventStreamExceptionCode,
|
||||
} from 'src/engine/subscriptions/event-stream.exception';
|
||||
import { type EventStreamData } from 'src/engine/subscriptions/types/event-stream-data.type';
|
||||
import {
|
||||
type EventStreamData,
|
||||
type RecordOrMetadataGqlOperationSignature,
|
||||
} from 'src/engine/subscriptions/types/event-stream-data.type';
|
||||
|
||||
@Injectable()
|
||||
export class EventStreamService implements OnModuleInit {
|
||||
@@ -180,7 +182,7 @@ export class EventStreamService implements OnModuleInit {
|
||||
workspaceId: string;
|
||||
eventStreamChannelId: string;
|
||||
queryId: string;
|
||||
operationSignature: RecordGqlOperationSignature;
|
||||
operationSignature: RecordOrMetadataGqlOperationSignature;
|
||||
}): Promise<void> {
|
||||
const key = this.getEventStreamKey(workspaceId, eventStreamChannelId);
|
||||
const existing = await this.cacheStorageService.get<EventStreamData>(key);
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import { type RecordGqlOperationSignature } from 'twenty-shared/types';
|
||||
import {
|
||||
type MetadataGqlOperationSignature,
|
||||
type RecordGqlOperationSignature,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { type SerializableAuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
|
||||
export type RecordOrMetadataGqlOperationSignature =
|
||||
| RecordGqlOperationSignature
|
||||
| MetadataGqlOperationSignature;
|
||||
|
||||
export type EventStreamData = {
|
||||
authContext: SerializableAuthContext;
|
||||
workspaceId: string;
|
||||
queries: Record<string, RecordGqlOperationSignature>;
|
||||
queries: Record<string, RecordOrMetadataGqlOperationSignature>;
|
||||
createdAt: number;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { type ObjectRecordSubscriptionEvent } from 'src/engine/subscriptions/types/object-record-subscription-event.type';
|
||||
import { type MetadataEvent } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/metadata-event';
|
||||
|
||||
export type EventStreamPayload = {
|
||||
objectRecordEventsWithQueryIds: {
|
||||
queryIds: string[];
|
||||
objectRecordEvent: ObjectRecordSubscriptionEvent;
|
||||
}[];
|
||||
metadataEventsWithQueryIds: {
|
||||
queryIds: string[];
|
||||
metadataEvent: MetadataEvent;
|
||||
}[];
|
||||
};
|
||||
Reference in New Issue
Block a user