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:
martmull
2025-04-17 16:03:51 +02:00
committed by GitHub
parent b112d06f66
commit 42e060ac74
25 changed files with 552 additions and 27 deletions

View File

@ -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;
};

View File

@ -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
}
}
`;

View File

@ -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]);
};