Ws poc (#11293)
related to https://github.com/twentyhq/core-team-issues/issues/601 ## Done - add a `onDbEvent` `Subscription` graphql endpoint to listen to database_event using what we have done with webhooks: - you can subscribe to any `action` (created, updated, ...) for any `objectNameSingular` or a specific `recordId`. Parameters are nullable and treated as wildcards when null. - returns events with following shape ```typescript @Field(() => String) eventId: string; @Field() emittedAt: string; @Field(() => DatabaseEventAction) action: DatabaseEventAction; @Field(() => String) objectNameSingular: string; @Field(() => GraphQLJSON) record: ObjectRecord; @Field(() => [String], { nullable: true }) updatedFields?: string[]; ``` - front provide a componentEffect `<ListenRecordUpdatesEffect />` that listen for an `objectNameSingular`, a `recordId` and a list of `listenedFields`. It subscribes to record updates and updates its apollo cached value for specified `listenedFields` - subscription is protected with credentials ## Result Here is an application with `workflowRun` https://github.com/user-attachments/assets/c964d857-3b54-495f-bf14-587ba26c5a8c --------- Co-authored-by: prastoin <paul@twenty.com>
This commit is contained in:
@ -23,6 +23,7 @@ import { cookieStorage } from '~/utils/cookie-storage';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
import { ApolloManager } from '../types/apolloManager.interface';
|
||||
import { loggerLink } from '../utils/loggerLink';
|
||||
import { getTokenPair } from '../utils/getTokenPair';
|
||||
|
||||
const logger = loggerLink(() => 'Twenty');
|
||||
|
||||
@ -55,14 +56,6 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
|
||||
|
||||
this.currentWorkspaceMember = currentWorkspaceMember;
|
||||
|
||||
const getTokenPair = () => {
|
||||
const stringTokenPair = cookieStorage.getItem('tokenPair');
|
||||
const tokenPair = isDefined(stringTokenPair)
|
||||
? (JSON.parse(stringTokenPair) as AuthTokenPair)
|
||||
: undefined;
|
||||
return tokenPair;
|
||||
};
|
||||
|
||||
const buildApolloLink = (): ApolloLink => {
|
||||
const httpLink = createUploadLink({
|
||||
uri,
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
import { cookieStorage } from '~/utils/cookie-storage';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { AuthTokenPair } from '~/generated/graphql';
|
||||
|
||||
export const getTokenPair = () => {
|
||||
const stringTokenPair = cookieStorage.getItem('tokenPair');
|
||||
return isDefined(stringTokenPair)
|
||||
? (JSON.parse(stringTokenPair) as AuthTokenPair)
|
||||
: undefined;
|
||||
};
|
||||
@ -15,6 +15,7 @@ import { WorkflowVersionVisualizerEffect } from '@/workflow/workflow-diagram/com
|
||||
import { WorkflowVisualizer } from '@/workflow/workflow-diagram/components/WorkflowVisualizer';
|
||||
import { WorkflowVisualizerEffect } from '@/workflow/workflow-diagram/components/WorkflowVisualizerEffect';
|
||||
import styled from '@emotion/styled';
|
||||
import { ListenRecordUpdatesEffect } from '@/subscription/components/ListenUpdatesEffect';
|
||||
|
||||
const StyledGreyBox = styled.div<{ isInRightDrawer?: boolean }>`
|
||||
background: ${({ theme, isInRightDrawer }) =>
|
||||
@ -99,6 +100,11 @@ export const CardComponents: Record<CardType, CardComponentType> = {
|
||||
[CardType.WorkflowRunCard]: ({ targetableObject }) => (
|
||||
<>
|
||||
<WorkflowRunVisualizerEffect workflowRunId={targetableObject.id} />
|
||||
<ListenRecordUpdatesEffect
|
||||
objectNameSingular={targetableObject.targetObjectNameSingular}
|
||||
recordId={targetableObject.id}
|
||||
listenedFields={['status', 'output']}
|
||||
/>
|
||||
|
||||
<WorkflowRunVisualizer workflowRunId={targetableObject.id} />
|
||||
</>
|
||||
|
||||
@ -0,0 +1,45 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { useOnDbEvent } from '@/subscription/hooks/useOnDbEvent';
|
||||
import { DatabaseEventAction } from '~/generated/graphql';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type ListenRecordUpdatesEffectProps = {
|
||||
objectNameSingular: string;
|
||||
recordId: string;
|
||||
listenedFields: string[];
|
||||
};
|
||||
|
||||
export const ListenRecordUpdatesEffect = ({
|
||||
objectNameSingular,
|
||||
recordId,
|
||||
listenedFields,
|
||||
}: ListenRecordUpdatesEffectProps) => {
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
useOnDbEvent({
|
||||
input: { recordId, action: DatabaseEventAction.UPDATED },
|
||||
onData: (data) => {
|
||||
const updatedRecord = data.onDbEvent.record;
|
||||
|
||||
const fieldsUpdater = listenedFields.reduce((acc, listenedField) => {
|
||||
if (!isDefined(updatedRecord[listenedField])) {
|
||||
return acc;
|
||||
}
|
||||
return {
|
||||
...acc,
|
||||
[listenedField]: () => updatedRecord[listenedField],
|
||||
};
|
||||
}, {});
|
||||
|
||||
apolloClient.cache.modify({
|
||||
id: apolloClient.cache.identify({
|
||||
__typename: capitalize(objectNameSingular),
|
||||
id: recordId,
|
||||
}),
|
||||
fields: fieldsUpdater,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
@ -0,0 +1,13 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const ON_DB_EVENT = gql`
|
||||
subscription OnDbEvent($input: OnDbEventInput!) {
|
||||
onDbEvent(input: $input) {
|
||||
eventDate
|
||||
action
|
||||
objectNameSingular
|
||||
updatedFields
|
||||
record
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,58 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { createClient } from 'graphql-sse';
|
||||
import { ON_DB_EVENT } from '@/subscription/graphql/subscriptions/onDbEvent';
|
||||
import { Subscription, SubscriptionOnDbEventArgs } from '~/generated/graphql';
|
||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
import { getTokenPair } from '@/apollo/utils/getTokenPair';
|
||||
|
||||
type OnDbEventArgs = SubscriptionOnDbEventArgs & {
|
||||
skip?: boolean;
|
||||
onData?: (data: Subscription) => void;
|
||||
onError?: (err: any) => void;
|
||||
onComplete?: () => void;
|
||||
};
|
||||
|
||||
export const useOnDbEvent = ({
|
||||
onData,
|
||||
onError,
|
||||
onComplete,
|
||||
input,
|
||||
skip = false,
|
||||
}: OnDbEventArgs) => {
|
||||
const tokenPair = getTokenPair();
|
||||
|
||||
const sseClient = useMemo(() => {
|
||||
return createClient({
|
||||
url: `${REACT_APP_SERVER_BASE_URL}/graphql`,
|
||||
headers: {
|
||||
Authorization: tokenPair?.accessToken.token
|
||||
? `Bearer ${tokenPair?.accessToken.token}`
|
||||
: '',
|
||||
},
|
||||
});
|
||||
}, [tokenPair?.accessToken.token]);
|
||||
|
||||
useEffect(() => {
|
||||
if (skip === true) {
|
||||
return;
|
||||
}
|
||||
const next = (value: { data: Subscription }) => onData?.(value.data);
|
||||
const error = (err: unknown) => onError?.(err);
|
||||
const complete = () => onComplete?.();
|
||||
const unsubscribe = sseClient.subscribe(
|
||||
{
|
||||
query: ON_DB_EVENT.loc?.source.body || '',
|
||||
variables: { input },
|
||||
},
|
||||
{
|
||||
next,
|
||||
error,
|
||||
complete,
|
||||
},
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [input, onComplete, onData, onError, skip, sseClient]);
|
||||
};
|
||||
Reference in New Issue
Block a user