From d1456849669a15b27f62671b847d5ce652e21be8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Fri, 19 Apr 2024 17:52:57 +0200 Subject: [PATCH] New Timeline (#4936) Refactored the code to introduce two different concepts: - AuditLogs (immutable, raw data) - TimelineActivities (user-friendly, transformed data) Still some work needed: - Add message, files, calendar events to timeline (~2 hours if done naively) - Refactor repository to try to abstract concept when we can (tbd, wait for Twenty ORM) - Introduce ability to display child timelines on parent timeline with filtering (~2 days) - Improve UI: add links to open note/task, improve diff display, etc (half a day) - Decide the path forward for Task vs Notes: either introduce a new field type "Record Type" and start going into that direction ; or split in two objects? - Trigger updates when a field is changed (will be solved by real-time / websockets: 2 weeks) - Integrate behavioral events (1 day for POC, 1 week for clean/documented) Screenshot 2024-04-12 at 09 24 49 --- .../timeline/hooks/useLinkedObject.ts | 16 ++ .../components/EventList.tsx | 15 +- .../components/EventRow.tsx | 172 ++++++++++++----- .../components/EventUpdateProperty.tsx | 5 +- .../components/EventsGroup.tsx | 12 +- .../components/TimelineActivities.tsx} | 18 +- .../__tests__/useTimelineActivities.test.ts} | 46 +---- .../hooks/useTimelineActivities.tsx} | 13 +- .../types/TimelineActivity.ts} | 8 +- .../__tests__/groupEventsByMonth.test.ts | 4 +- .../utils/groupEventsByMonth.ts | 6 +- .../objectMetadataItemFamilySelector.ts | 1 - .../types/CoreObjectNameSingular.ts | 2 +- .../object-record/hooks/useFindManyRecords.ts | 5 +- .../components/ShowPageRightContainer.tsx | 6 +- .../{events.ts => timeline-activities.ts} | 16 +- .../listeners/entity-events-to-db.listener.ts | 45 ++--- .../listeners/record-position.listener.ts | 2 +- .../workspace-query-runner.module.ts | 6 +- .../workspace-query-runner.service.ts | 12 +- .../user-workspace/user-workspace.service.ts | 2 +- .../workspace-workspace-member.listener.ts | 2 +- .../types/object-record-create.event.ts | 2 +- .../types/object-record-delete.event.ts | 2 +- .../types/object-record-job-data.ts | 11 ++ .../types/object-record-update.event.ts | 2 +- .../types/object-record.base.event.ts | 4 +- .../object-record-changed-values.spec.ts | 54 ++++-- .../utils/object-record-changed-values.ts | 23 ++- .../utils/object-record-diff-merge.ts | 30 +++ .../integrations/message-queue/jobs.module.ts | 31 ++-- .../object-metadata.service.ts | 155 ++++++++-------- .../metadata-to-repository.mapping.ts | 6 +- .../constants/standard-field-ids.ts | 43 ++++- .../constants/standard-object-ids.ts | 5 +- .../custom-objects/custom.object-metadata.ts | 16 +- .../standard-objects/index.ts | 16 +- .../listeners/participant-person.listener.ts | 12 +- .../participant-workspace-member.listener.ts | 14 +- .../listeners/calendar-channel.listener.ts | 6 +- .../company.object-metadata.ts | 12 +- .../standard-objects/event.object-metadata.ts | 3 + .../messaging-message-channel.listener.ts | 6 +- .../opportunity.object-metadata.ts | 12 +- .../person.object-metadata.ts | 8 +- .../create-audit-log-from-internal-event.ts} | 40 ++-- ...timeline-activity-from-behavioral-event.ts | 1 + ...meline-activity-from-internal-event.job.ts | 50 +++++ .../repositiories/audit-log.repository.ts} | 20 +- .../timeline-activity.repository.ts | 136 ++++++++++++++ .../services/timeline-activity.service.ts | 173 ++++++++++++++++++ .../audit-log.object-metadata.ts | 96 ++++++++++ .../behavioral-event.object-metadata.ts | 90 +++++++++ .../timeline-activity.object-metadata.ts | 148 +++++++++++++++ .../timeline/timeline-activity.module.ts | 16 ++ .../workspace-member.object-metadata.ts | 25 ++- 56 files changed, 1314 insertions(+), 368 deletions(-) create mode 100644 packages/twenty-front/src/modules/activities/timeline/hooks/useLinkedObject.ts rename packages/twenty-front/src/modules/activities/{events => timelineActivities}/components/EventList.tsx (70%) rename packages/twenty-front/src/modules/activities/{events => timelineActivities}/components/EventRow.tsx (50%) rename packages/twenty-front/src/modules/activities/{events => timelineActivities}/components/EventUpdateProperty.tsx (84%) rename packages/twenty-front/src/modules/activities/{events => timelineActivities}/components/EventsGroup.tsx (82%) rename packages/twenty-front/src/modules/activities/{events/components/Events.tsx => timelineActivities/components/TimelineActivities.tsx} (69%) rename packages/twenty-front/src/modules/activities/{events/hooks/__tests__/useEvents.test.ts => timelineActivities/hooks/__tests__/useTimelineActivities.test.ts} (56%) rename packages/twenty-front/src/modules/activities/{events/hooks/useEvents.tsx => timelineActivities/hooks/useTimelineActivities.tsx} (63%) rename packages/twenty-front/src/modules/activities/{events/types/Event.ts => timelineActivities/types/TimelineActivity.ts} (65%) rename packages/twenty-front/src/modules/activities/{events => timelineActivities}/utils/__tests__/groupEventsByMonth.test.ts (76%) rename packages/twenty-front/src/modules/activities/{events => timelineActivities}/utils/groupEventsByMonth.ts (78%) rename packages/twenty-front/src/testing/mock-data/{events.ts => timeline-activities.ts} (81%) create mode 100644 packages/twenty-server/src/engine/integrations/event-emitter/types/object-record-job-data.ts create mode 100644 packages/twenty-server/src/engine/integrations/event-emitter/utils/object-record-diff-merge.ts rename packages/twenty-server/src/{engine/api/graphql/workspace-query-runner/jobs/save-event-to-db.job.ts => modules/timeline/jobs/create-audit-log-from-internal-event.ts} (56%) create mode 100644 packages/twenty-server/src/modules/timeline/jobs/upsert-timeline-activity-from-behavioral-event.ts create mode 100644 packages/twenty-server/src/modules/timeline/jobs/upsert-timeline-activity-from-internal-event.job.ts rename packages/twenty-server/src/modules/{event/repositiories/event.repository.ts => timeline/repositiories/audit-log.repository.ts} (60%) create mode 100644 packages/twenty-server/src/modules/timeline/repositiories/timeline-activity.repository.ts create mode 100644 packages/twenty-server/src/modules/timeline/services/timeline-activity.service.ts create mode 100644 packages/twenty-server/src/modules/timeline/standard-objects/audit-log.object-metadata.ts create mode 100644 packages/twenty-server/src/modules/timeline/standard-objects/behavioral-event.object-metadata.ts create mode 100644 packages/twenty-server/src/modules/timeline/standard-objects/timeline-activity.object-metadata.ts create mode 100644 packages/twenty-server/src/modules/timeline/timeline-activity.module.ts diff --git a/packages/twenty-front/src/modules/activities/timeline/hooks/useLinkedObject.ts b/packages/twenty-front/src/modules/activities/timeline/hooks/useLinkedObject.ts new file mode 100644 index 000000000..b628b5690 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/timeline/hooks/useLinkedObject.ts @@ -0,0 +1,16 @@ +import { useRecoilValue } from 'recoil'; + +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; + +export const useLinkedObject = (id: string) => { + const objectMetadataItems: ObjectMetadataItem[] = useRecoilValue( + objectMetadataItemsState, + ); + + return ( + objectMetadataItems.find( + (objectMetadataItem) => objectMetadataItem.id === id, + ) ?? null + ); +}; diff --git a/packages/twenty-front/src/modules/activities/events/components/EventList.tsx b/packages/twenty-front/src/modules/activities/timelineActivities/components/EventList.tsx similarity index 70% rename from packages/twenty-front/src/modules/activities/events/components/EventList.tsx rename to packages/twenty-front/src/modules/activities/timelineActivities/components/EventList.tsx index 92219b4e8..43f731264 100644 --- a/packages/twenty-front/src/modules/activities/events/components/EventList.tsx +++ b/packages/twenty-front/src/modules/activities/timelineActivities/components/EventList.tsx @@ -1,16 +1,17 @@ import { ReactElement } from 'react'; import styled from '@emotion/styled'; -import { EventsGroup } from '@/activities/events/components/EventsGroup'; -import { Event } from '@/activities/events/types/Event'; -import { groupEventsByMonth } from '@/activities/events/utils/groupEventsByMonth'; +import { EventsGroup } from '@/activities/timelineActivities/components/EventsGroup'; +import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; +import { groupEventsByMonth } from '@/activities/timelineActivities/utils/groupEventsByMonth'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; type EventListProps = { targetableObject: ActivityTargetableObject; title: string; - events: Event[]; + events: TimelineActivity[]; button?: ReactElement | false; }; @@ -31,12 +32,16 @@ const StyledTimelineContainer = styled.div` export const EventList = ({ events, targetableObject }: EventListProps) => { const groupedEvents = groupEventsByMonth(events); + const mainObjectMetadataItem = useObjectMetadataItem({ + objectNameSingular: targetableObject.targetObjectNameSingular, + }).objectMetadataItem; + return ( {groupedEvents.map((group, index) => ( (useIsMobile() ? 'wrap' : 'nowrap')}; - gap: ${({ theme }) => theme.spacing(1)}; - overflow: hidden; -`; - const StyledItemAuthorText = styled.span` display: flex; color: ${({ theme }) => theme.font.color.primary}; @@ -65,6 +67,11 @@ const StyledItemTitle = styled.span` white-space: nowrap; `; +const StyledLinkedObject = styled.span` + cursor: pointer; + text-decoration: underline; +`; + const StyledItemTitleDate = styled.div` align-items: center; color: ${({ theme }) => theme.font.color.tertiary}; @@ -117,16 +124,24 @@ const StyledTimelineItemContainer = styled.div<{ isGap?: boolean }>` white-space: nowrap; `; +const StyledSummary = styled.summary` + display: flex; + flex: 1; + flex-flow: row ${() => (useIsMobile() ? 'wrap' : 'nowrap')}; + gap: ${({ theme }) => theme.spacing(1)}; + overflow: hidden; +`; + type EventRowProps = { - targetableObject: ActivityTargetableObject; + mainObjectMetadataItem: ObjectMetadataItem | null; isLastEvent?: boolean; - event: Event; + event: TimelineActivity; }; export const EventRow = ({ isLastEvent, event, - targetableObject, + mainObjectMetadataItem, }: EventRowProps) => { const beautifiedCreatedAt = beautifyPastDateRelativeToNow(event.createdAt); const exactCreatedAt = beautifyExactDateTime(event.createdAt); @@ -135,47 +150,108 @@ export const EventRow = ({ const diff: Record = properties?.diff; const isEventType = (type: 'created' | 'updated') => { - return ( - event.name === type + '.' + targetableObject.targetObjectNameSingular - ); + if (event.name.includes('.')) { + return event.name.split('.')[1] === type; + } + return false; }; + const { getIcon } = useIcons(); + + const linkedObjectMetadata = useLinkedObject(event.linkedObjectMetadataId); + + const linkedObjectLabel = event.name.includes('note') + ? 'note' + : event.name.includes('task') + ? 'task' + : linkedObjectMetadata?.labelSingular; + + const ActivityIcon = event.linkedObjectMetadataId + ? event.name.includes('note') + ? IconNotes + : event.name.includes('task') + ? IconCheckbox + : getIcon(linkedObjectMetadata?.icon) + : isEventType('created') + ? IconCirclePlus + : isEventType('updated') + ? IconEditCircle + : IconFocusCentered; + + const author = + event.workspaceMember?.name.firstName + + ' ' + + event.workspaceMember?.name.lastName; + + const action = isEventType('created') + ? 'created' + : isEventType('updated') + ? 'updated' + : event.name; + + let description; + + if (!isUndefinedOrNull(linkedObjectMetadata)) { + description = 'a ' + linkedObjectLabel; + } else if (!event.linkedObjectMetadataId && isEventType('created')) { + description = `a new ${mainObjectMetadataItem?.labelSingular}`; + } else if (isEventType('updated')) { + const diffKeys = Object.keys(diff); + if (diffKeys.length === 0) { + description = `a ${mainObjectMetadataItem?.labelSingular}`; + } else if (diffKeys.length === 1) { + const [key, value] = Object.entries(diff)[0]; + description = [ + , + ]; + } else if (diffKeys.length === 2) { + description = + mainObjectMetadataItem?.fields.find( + (field) => diffKeys[0] === field.name, + )?.label + + ' and ' + + mainObjectMetadataItem?.fields.find( + (field) => diffKeys[1] === field.name, + )?.label; + } else if (diffKeys.length > 2) { + description = + diffKeys[0] + ' and ' + (diffKeys.length - 1) + ' other fields'; + } + } else if (!isEventType('created') && !isEventType('updated')) { + description = JSON.stringify(diff); + } + const details = JSON.stringify(diff); + + const openActivityRightDrawer = useOpenActivityRightDrawer(); + return ( <> - {isEventType('created') && } - {isEventType('updated') && } - {!isEventType('created') && !isEventType('updated') && ( - - )} + - - - {event.workspaceMember?.name.firstName}{' '} - {event.workspaceMember?.name.lastName} - - - {isEventType('created') && 'created'} - {isEventType('updated') && 'updated'} - {!isEventType('created') && !isEventType('updated') && event.name} - - - {isEventType('created') && - `a new ${targetableObject.targetObjectNameSingular}`} - {isEventType('updated') && - Object.entries(diff).map(([key, value]) => ( - - ))} - {!isEventType('created') && - !isEventType('updated') && - JSON.stringify(diff)} - - +
+ + {author} + {action} + {description} + {isUndefinedOrNull(linkedObjectMetadata) ? ( + <> + ) : ( + openActivityRightDrawer(event.linkedRecordId)} + > + {event.linkedRecordCachedName} + + )} + + {details} +
+ {beautifiedCreatedAt} diff --git a/packages/twenty-front/src/modules/activities/events/components/EventUpdateProperty.tsx b/packages/twenty-front/src/modules/activities/timelineActivities/components/EventUpdateProperty.tsx similarity index 84% rename from packages/twenty-front/src/modules/activities/events/components/EventUpdateProperty.tsx rename to packages/twenty-front/src/modules/activities/timelineActivities/components/EventUpdateProperty.tsx index cbeb157e2..46f4061db 100644 --- a/packages/twenty-front/src/modules/activities/events/components/EventUpdateProperty.tsx +++ b/packages/twenty-front/src/modules/activities/timelineActivities/components/EventUpdateProperty.tsx @@ -4,6 +4,7 @@ import { IconArrowRight } from 'twenty-ui'; type EventUpdatePropertyProps = { propertyName: string; + before?: string; after?: string; }; @@ -23,9 +24,9 @@ export const EventUpdateProperty = ({ const theme = useTheme(); return ( - {propertyName} + {propertyName ?? '(empty)'} - {after} + {JSON.stringify(after)} ); }; diff --git a/packages/twenty-front/src/modules/activities/events/components/EventsGroup.tsx b/packages/twenty-front/src/modules/activities/timelineActivities/components/EventsGroup.tsx similarity index 82% rename from packages/twenty-front/src/modules/activities/events/components/EventsGroup.tsx rename to packages/twenty-front/src/modules/activities/timelineActivities/components/EventsGroup.tsx index 091e9913e..b441b7ab7 100644 --- a/packages/twenty-front/src/modules/activities/events/components/EventsGroup.tsx +++ b/packages/twenty-front/src/modules/activities/timelineActivities/components/EventsGroup.tsx @@ -1,14 +1,14 @@ import styled from '@emotion/styled'; -import { EventRow } from '@/activities/events/components/EventRow'; -import { EventGroup } from '@/activities/events/utils/groupEventsByMonth'; -import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; +import { EventRow } from '@/activities/timelineActivities/components/EventRow'; +import { EventGroup } from '@/activities/timelineActivities/utils/groupEventsByMonth'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; type EventsGroupProps = { group: EventGroup; month: string; year?: number; - targetableObject: ActivityTargetableObject; + mainObjectMetadataItem: ObjectMetadataItem | null; }; const StyledActivityGroup = styled.div` @@ -57,7 +57,7 @@ export const EventsGroup = ({ group, month, year, - targetableObject, + mainObjectMetadataItem, }: EventsGroupProps) => { return ( @@ -69,7 +69,7 @@ export const EventsGroup = ({ {group.items.map((event, index) => ( { - const { events } = useEvents(targetableObject); + const { timelineActivities } = useTimelineActivities(targetableObject); - if (!isNonEmptyArray(events)) { + if (!isNonEmptyArray(timelineActivities)) { return ( - No Events + Add your first Activity - There are no events associated with this record.{' '} + There are no activities associated with this record.{' '} + ); } @@ -53,7 +55,7 @@ export const Events = ({ ); diff --git a/packages/twenty-front/src/modules/activities/events/hooks/__tests__/useEvents.test.ts b/packages/twenty-front/src/modules/activities/timelineActivities/hooks/__tests__/useTimelineActivities.test.ts similarity index 56% rename from packages/twenty-front/src/modules/activities/events/hooks/__tests__/useEvents.test.ts rename to packages/twenty-front/src/modules/activities/timelineActivities/hooks/__tests__/useTimelineActivities.test.ts index 25c6d5bf1..1115a4c70 100644 --- a/packages/twenty-front/src/modules/activities/events/hooks/__tests__/useEvents.test.ts +++ b/packages/twenty-front/src/modules/activities/timelineActivities/hooks/__tests__/useTimelineActivities.test.ts @@ -1,53 +1,21 @@ import { renderHook } from '@testing-library/react'; -import { useEvents } from '@/activities/events/hooks/useEvents'; +import { useTimelineActivities } from '@/activities/timelineActivities/hooks/useTimelineActivities'; jest.mock('@/object-record/hooks/useFindManyRecords', () => ({ useFindManyRecords: jest.fn(), })); -describe('useEvent', () => { +describe('useTimelineActivities', () => { afterEach(() => { jest.clearAllMocks(); }); it('fetches events correctly for a given targetableObject', () => { - const mockEvents = [ + const mockedTimelineActivities = [ { __typename: 'Event', id: '166ec73f-26b1-4934-bb3b-c86c8513b99b', - opportunityId: null, - opportunity: null, - personId: null, - person: null, - company: { - __typename: 'Company', - address: 'Paris', - linkedinLink: { - __typename: 'Link', - label: '', - url: '', - }, - xLink: { - __typename: 'Link', - label: '', - url: '', - }, - position: 4, - domainName: 'microsoft.com', - employees: null, - createdAt: '2024-03-21T16:01:41.809Z', - annualRecurringRevenue: { - __typename: 'Currency', - amountMicros: 100000000, - currencyCode: 'USD', - }, - idealCustomerProfile: false, - accountOwnerId: null, - updatedAt: '2024-03-22T08:28:44.812Z', - name: 'Microsoft', - id: '460b6fb1-ed89-413a-b31a-962986e67bb4', - }, workspaceMember: { __typename: 'WorkspaceMember', locale: 'en', @@ -81,11 +49,13 @@ describe('useEvent', () => { '@/object-record/hooks/useFindManyRecords', ); useFindManyRecordsMock.useFindManyRecords.mockReturnValue({ - records: mockEvents, + records: mockedTimelineActivities, }); - const { result } = renderHook(() => useEvents(mockTargetableObject)); + const { result } = renderHook(() => + useTimelineActivities(mockTargetableObject), + ); - expect(result.current.events).toEqual(mockEvents); + expect(result.current.timelineActivities).toEqual(mockedTimelineActivities); }); }); diff --git a/packages/twenty-front/src/modules/activities/events/hooks/useEvents.tsx b/packages/twenty-front/src/modules/activities/timelineActivities/hooks/useTimelineActivities.tsx similarity index 63% rename from packages/twenty-front/src/modules/activities/events/hooks/useEvents.tsx rename to packages/twenty-front/src/modules/activities/timelineActivities/hooks/useTimelineActivities.tsx index 9e5cbecc3..d80726468 100644 --- a/packages/twenty-front/src/modules/activities/events/hooks/useEvents.tsx +++ b/packages/twenty-front/src/modules/activities/timelineActivities/hooks/useTimelineActivities.tsx @@ -1,17 +1,19 @@ -import { Event } from '@/activities/events/types/Event'; +import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; // do we need to test this? -export const useEvents = (targetableObject: ActivityTargetableObject) => { +export const useTimelineActivities = ( + targetableObject: ActivityTargetableObject, +) => { const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({ nameSingular: targetableObject.targetObjectNameSingular, }); - const { records: events } = useFindManyRecords({ - objectNameSingular: CoreObjectNameSingular.Event, + const { records: TimelineActivities } = useFindManyRecords({ + objectNameSingular: CoreObjectNameSingular.TimelineActivity, filter: { [targetableObjectFieldIdName]: { eq: targetableObject.id, @@ -20,9 +22,10 @@ export const useEvents = (targetableObject: ActivityTargetableObject) => { orderBy: { createdAt: 'DescNullsFirst', }, + fetchPolicy: 'network-only', }); return { - events: events as Event[], + timelineActivities: TimelineActivities as TimelineActivity[], }; }; diff --git a/packages/twenty-front/src/modules/activities/events/types/Event.ts b/packages/twenty-front/src/modules/activities/timelineActivities/types/TimelineActivity.ts similarity index 65% rename from packages/twenty-front/src/modules/activities/events/types/Event.ts rename to packages/twenty-front/src/modules/activities/timelineActivities/types/TimelineActivity.ts index c39ceecd2..d7cf52b68 100644 --- a/packages/twenty-front/src/modules/activities/events/types/Event.ts +++ b/packages/twenty-front/src/modules/activities/timelineActivities/types/TimelineActivity.ts @@ -1,15 +1,15 @@ import { WorkspaceMember } from '~/generated/graphql'; -export type Event = { +export type TimelineActivity = { id: string; createdAt: string; updatedAt: string; deletedAt: string | null; - opportunityId: string | null; - companyId: string | null; - personId: string | null; workspaceMemberId: string; workspaceMember: WorkspaceMember; properties: any; name: string; + linkedRecordCachedName: string; + linkedRecordId: string; + linkedObjectMetadataId: string; }; diff --git a/packages/twenty-front/src/modules/activities/events/utils/__tests__/groupEventsByMonth.test.ts b/packages/twenty-front/src/modules/activities/timelineActivities/utils/__tests__/groupEventsByMonth.test.ts similarity index 76% rename from packages/twenty-front/src/modules/activities/events/utils/__tests__/groupEventsByMonth.test.ts rename to packages/twenty-front/src/modules/activities/timelineActivities/utils/__tests__/groupEventsByMonth.test.ts index 419e40a58..b88b89c1d 100644 --- a/packages/twenty-front/src/modules/activities/events/utils/__tests__/groupEventsByMonth.test.ts +++ b/packages/twenty-front/src/modules/activities/timelineActivities/utils/__tests__/groupEventsByMonth.test.ts @@ -1,10 +1,10 @@ -import { mockedEvents } from '~/testing/mock-data/events'; +import { mockedTimelineActivities } from '~/testing/mock-data/timeline-activities'; import { groupEventsByMonth } from '../groupEventsByMonth'; describe('groupEventsByMonth', () => { it('should group activities by month', () => { - const grouped = groupEventsByMonth(mockedEvents); + const grouped = groupEventsByMonth(mockedTimelineActivities); expect(grouped).toHaveLength(2); expect(grouped[0].items).toHaveLength(1); diff --git a/packages/twenty-front/src/modules/activities/events/utils/groupEventsByMonth.ts b/packages/twenty-front/src/modules/activities/timelineActivities/utils/groupEventsByMonth.ts similarity index 78% rename from packages/twenty-front/src/modules/activities/events/utils/groupEventsByMonth.ts rename to packages/twenty-front/src/modules/activities/timelineActivities/utils/groupEventsByMonth.ts index 316bd25e8..fa0779f53 100644 --- a/packages/twenty-front/src/modules/activities/events/utils/groupEventsByMonth.ts +++ b/packages/twenty-front/src/modules/activities/timelineActivities/utils/groupEventsByMonth.ts @@ -1,13 +1,13 @@ -import { Event } from '@/activities/events/types/Event'; +import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; import { isDefined } from '~/utils/isDefined'; export type EventGroup = { month: number; year: number; - items: Event[]; + items: TimelineActivity[]; }; -export const groupEventsByMonth = (events: Event[]) => { +export const groupEventsByMonth = (events: TimelineActivity[]) => { const acitivityGroups: EventGroup[] = []; for (const event of events) { diff --git a/packages/twenty-front/src/modules/object-metadata/states/objectMetadataItemFamilySelector.ts b/packages/twenty-front/src/modules/object-metadata/states/objectMetadataItemFamilySelector.ts index b264758fb..5409526ec 100644 --- a/packages/twenty-front/src/modules/object-metadata/states/objectMetadataItemFamilySelector.ts +++ b/packages/twenty-front/src/modules/object-metadata/states/objectMetadataItemFamilySelector.ts @@ -33,7 +33,6 @@ export const objectMetadataItemFamilySelector = selectorFamily< ) ?? null ); } - return null; }, }); diff --git a/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts b/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts index 18405ff22..5fafc5c64 100644 --- a/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts +++ b/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts @@ -9,7 +9,7 @@ export enum CoreObjectNameSingular { Comment = 'comment', Company = 'company', ConnectedAccount = 'connectedAccount', - Event = 'event', + TimelineActivity = 'timelineActivity', Favorite = 'favorite', Message = 'message', MessageChannel = 'messageChannel', diff --git a/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts index c32e0c17f..535373a15 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts @@ -1,5 +1,5 @@ import { useCallback, useMemo } from 'react'; -import { useQuery } from '@apollo/client'; +import { useQuery, WatchQueryFetchPolicy } from '@apollo/client'; import { isNonEmptyArray } from '@apollo/client/utilities'; import { isNonEmptyString } from '@sniptt/guards'; import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; @@ -32,6 +32,7 @@ export const useFindManyRecords = ({ onCompleted, skip, queryFields, + fetchPolicy, }: ObjectMetadataItemIdentifier & ObjectRecordQueryVariables & { onCompleted?: ( @@ -44,6 +45,7 @@ export const useFindManyRecords = ({ skip?: boolean; depth?: number; queryFields?: Record; + fetchPolicy?: WatchQueryFetchPolicy; }) => { const findManyQueryStateIdentifier = objectNameSingular + @@ -84,6 +86,7 @@ export const useFindManyRecords = ({ limit, orderBy, }, + fetchPolicy: fetchPolicy, onCompleted: (data) => { if (!isDefined(data)) { onCompleted?.([]); diff --git a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageRightContainer.tsx b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageRightContainer.tsx index 0362a13e5..f665994b8 100644 --- a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageRightContainer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageRightContainer.tsx @@ -11,12 +11,12 @@ import { import { Calendar } from '@/activities/calendar/components/Calendar'; import { EmailThreads } from '@/activities/emails/components/EmailThreads'; -import { Events } from '@/activities/events/components/Events'; import { Attachments } from '@/activities/files/components/Attachments'; import { Notes } from '@/activities/notes/components/Notes'; import { ObjectTasks } from '@/activities/tasks/components/ObjectTasks'; import { Timeline } from '@/activities/timeline/components/Timeline'; import { TimelineQueryEffect } from '@/activities/timeline/components/TimelineQueryEffect'; +import { TimelineActivities } from '@/activities/timelineActivities/components/TimelineActivities'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { TabList } from '@/ui/layout/tab/components/TabList'; @@ -144,7 +144,9 @@ export const ShowPageRightContainer = ({ {activeTabId === 'calendar' && ( )} - {activeTabId === 'logs' && } + {activeTabId === 'logs' && ( + + )} ); }; diff --git a/packages/twenty-front/src/testing/mock-data/events.ts b/packages/twenty-front/src/testing/mock-data/timeline-activities.ts similarity index 81% rename from packages/twenty-front/src/testing/mock-data/events.ts rename to packages/twenty-front/src/testing/mock-data/timeline-activities.ts index 2be81b414..35df59b20 100644 --- a/packages/twenty-front/src/testing/mock-data/events.ts +++ b/packages/twenty-front/src/testing/mock-data/timeline-activities.ts @@ -1,14 +1,14 @@ -import { Event } from '@/activities/events/types/Event'; +import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; -export const mockedEvents: Array = [ +export const mockedTimelineActivities: Array = [ { properties: '{"diff": {"address": {"after": "TEST", "before": ""}}}', updatedAt: '2023-04-26T10:12:42.33625+00:00', id: '79f84835-b2f9-4ab5-8ab9-17dbcc45dda3', - personId: null, - companyId: 'ce40eca0-8f4b-4bba-ba91-5cbd870c64d0', + linkedObjectMetadataId: '1ad72a42-6ab4-4474-a62a-a57cae3c0298', + linkedRecordCachedName: 'Test', + linkedRecordId: 'ce40eca0-8f4b-4bba-ba91-5cbd870c64d0', name: 'updated.company', - opportunityId: null, createdAt: '2023-04-26T10:12:42.33625+00:00', workspaceMember: { __typename: 'WorkspaceMember', @@ -30,10 +30,10 @@ export const mockedEvents: Array = [ '{"after": {"id": "ce40eca0-8f4b-4bba-ba91-5cbd870c64d0", "name": "", "xLink": {"url": "", "label": ""}, "events": {"edges": [], "__typename": "eventConnection"}, "people": {"edges": [], "__typename": "personConnection"}, "address": "", "position": 0.5, "createdAt": "2024-03-24T21:33:45.765295", "employees": null, "favorites": {"edges": [], "__typename": "favoriteConnection"}, "updatedAt": "2024-03-24T21:33:45.765295", "__typename": "company", "domainName": "", "attachments": {"edges": [], "__typename": "attachmentConnection"}, "accountOwner": null, "linkedinLink": {"url": "", "label": ""}, "opportunities": {"edges": [], "__typename": "opportunityConnection"}, "accountOwnerId": null, "activityTargets": {"edges": [], "__typename": "activityTargetConnection"}, "idealCustomerProfile": false, "annualRecurringRevenue": {"amountMicros": null, "currencyCode": ""}}}', updatedAt: new Date().toISOString(), id: '1ad72a42-6ab4-4474-a62a-a57cae3c0298', - personId: null, - companyId: 'ce40eca0-8f4b-4bba-ba91-5cbd870c64d0', name: 'created.company', - opportunityId: null, + linkedObjectMetadataId: '1ad72a42-6ab4-4474-a62a-a57cae3c0298', + linkedRecordCachedName: 'Test', + linkedRecordId: 'ce40eca0-8f4b-4bba-ba91-5cbd870c64d0', createdAt: new Date().toISOString(), workspaceMember: { __typename: 'WorkspaceMember', diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/entity-events-to-db.listener.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/entity-events-to-db.listener.ts index cd7d0ac59..6ac775c85 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/entity-events-to-db.listener.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/entity-events-to-db.listener.ts @@ -7,16 +7,15 @@ import { Repository } from 'typeorm'; import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service'; import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event'; -import { - SaveEventToDbJobData, - SaveEventToDbJob, -} from 'src/engine/api/graphql/workspace-query-runner/jobs/save-event-to-db.job'; +import { CreateAuditLogFromInternalEvent } from 'src/modules/timeline/jobs/create-audit-log-from-internal-event'; import { FeatureFlagEntity, FeatureFlagKeys, } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { objectRecordChangedValues } from 'src/engine/integrations/event-emitter/utils/object-record-changed-values'; import { ObjectRecordUpdateEvent } from 'src/engine/integrations/event-emitter/types/object-record-update.event'; +import { ObjectRecordBaseEvent } from 'src/engine/integrations/event-emitter/types/object-record.base.event'; +import { UpsertTimelineActivityFromInternalEvent } from 'src/modules/timeline/jobs/upsert-timeline-activity-from-internal-event.job'; @Injectable() export class EntityEventsToDbListener { @@ -29,26 +28,27 @@ export class EntityEventsToDbListener { @OnEvent('*.created') async handleCreate(payload: ObjectRecordCreateEvent) { - return this.handle(payload, 'created'); + return this.handle(payload); } @OnEvent('*.updated') async handleUpdate(payload: ObjectRecordUpdateEvent) { - payload.details.diff = objectRecordChangedValues( - payload.details.before, - payload.details.after, + payload.properties.diff = objectRecordChangedValues( + payload.properties.before, + payload.properties.after, + payload.objectMetadata, ); - return this.handle(payload, 'updated'); + return this.handle(payload); } - // @OnEvent('*.deleted') - TODO: implement when we have soft deleted + // @OnEvent('*.deleted') - TODO: implement when we soft delete has been implemented // .... - private async handle( - payload: ObjectRecordCreateEvent, - operation: string, - ) { + // @OnEvent('*.restored') - TODO: implement when we soft delete has been implemented + // .... + + private async handle(payload: ObjectRecordCreateEvent) { if (!payload.objectMetadata.isAuditLogged) { return; } @@ -67,13 +67,14 @@ export class EntityEventsToDbListener { return; } - this.messageQueueService.add(SaveEventToDbJob.name, { - workspaceId: payload.workspaceId, - userId: payload.userId, - recordId: payload.recordId, - objectName: payload.objectMetadata.nameSingular, - operation: operation, - details: payload.details, - }); + this.messageQueueService.add( + CreateAuditLogFromInternalEvent.name, + payload, + ); + + this.messageQueueService.add( + UpsertTimelineActivityFromInternalEvent.name, + payload, + ); } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/record-position.listener.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/record-position.listener.ts index 9d9ea3803..2ea9d8afb 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/record-position.listener.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/record-position.listener.ts @@ -24,7 +24,7 @@ export class RecordPositionListener { return; } - if (hasPositionSet(payload.details.after)) { + if (hasPositionSet(payload.properties.after)) { return; } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts index 9b4efe05b..eccae0920 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts @@ -9,7 +9,6 @@ import { RecordPositionListener } from 'src/engine/api/graphql/workspace-query-r import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { EventObjectMetadata } from 'src/modules/event/standard-objects/event.object-metadata'; import { WorkspaceMemberObjectMetadata } from 'src/modules/workspace-member/standard-objects/workspace-member.object-metadata'; import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; @@ -24,10 +23,7 @@ import { EntityEventsToDbListener } from './listeners/entity-events-to-db.listen WorkspaceDataSourceModule, WorkspacePreQueryHookModule, TypeOrmModule.forFeature([Workspace, FeatureFlagEntity], 'core'), - ObjectMetadataRepositoryModule.forFeature([ - WorkspaceMemberObjectMetadata, - EventObjectMetadata, - ]), + ObjectMetadataRepositoryModule.forFeature([WorkspaceMemberObjectMetadata]), ], providers: [ WorkspaceQueryRunnerService, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts index 712372e44..0b3c37b65 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts @@ -249,11 +249,12 @@ export class WorkspaceQueryRunnerService { parsedResults.forEach((record) => { this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.created`, { + name: `${objectMetadataItem.nameSingular}.created`, workspaceId, userId, recordId: record.id, objectMetadata: objectMetadataItem, - details: { + properties: { after: record, }, } satisfies ObjectRecordCreateEvent); @@ -306,11 +307,12 @@ export class WorkspaceQueryRunnerService { ); this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.updated`, { + name: `${objectMetadataItem.nameSingular}.updated`, workspaceId, userId, recordId: (existingRecord as Record).id, objectMetadata: objectMetadataItem, - details: { + properties: { before: this.removeNestedProperties(existingRecord as Record), after: this.removeNestedProperties(parsedResults?.[0]), }, @@ -397,11 +399,12 @@ export class WorkspaceQueryRunnerService { parsedResults.forEach((record) => { this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.deleted`, { + name: `${objectMetadataItem.nameSingular}.deleted`, workspaceId, userId, recordId: record.id, objectMetadata: objectMetadataItem, - details: { + properties: { before: [this.removeNestedProperties(record)], }, } satisfies ObjectRecordDeleteEvent); @@ -448,11 +451,12 @@ export class WorkspaceQueryRunnerService { ); this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.deleted`, { + name: `${objectMetadataItem.nameSingular}.deleted`, workspaceId, userId, recordId: args.id, objectMetadata: objectMetadataItem, - details: { + properties: { before: { ...(deletedWorkspaceMember ?? {}), ...this.removeNestedProperties(parsedResults?.[0]), diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts index 30703d0d3..e61d1337f 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts @@ -62,7 +62,7 @@ export class UserWorkspaceService extends TypeOrmQueryService { new ObjectRecordCreateEvent(); payload.workspaceId = workspaceId; - payload.details = { + payload.properties = { after: workspaceMember[0], }; payload.recordId = workspaceMember[0].id; diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace-workspace-member.listener.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace-workspace-member.listener.ts index 3b6982cfe..3c93e6938 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace-workspace-member.listener.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace-workspace-member.listener.ts @@ -21,7 +21,7 @@ export class WorkspaceWorkspaceMemberListener { async handleDeleteEvent( payload: ObjectRecordDeleteEvent, ) { - const userId = payload.details.before.userId; + const userId = payload.properties.before.userId; if (!userId) { return; diff --git a/packages/twenty-server/src/engine/integrations/event-emitter/types/object-record-create.event.ts b/packages/twenty-server/src/engine/integrations/event-emitter/types/object-record-create.event.ts index 3e72f4fa3..0e3d6e22c 100644 --- a/packages/twenty-server/src/engine/integrations/event-emitter/types/object-record-create.event.ts +++ b/packages/twenty-server/src/engine/integrations/event-emitter/types/object-record-create.event.ts @@ -1,7 +1,7 @@ import { ObjectRecordBaseEvent } from 'src/engine/integrations/event-emitter/types/object-record.base.event'; export class ObjectRecordCreateEvent extends ObjectRecordBaseEvent { - details: { + properties: { after: T; }; } diff --git a/packages/twenty-server/src/engine/integrations/event-emitter/types/object-record-delete.event.ts b/packages/twenty-server/src/engine/integrations/event-emitter/types/object-record-delete.event.ts index b02f0a87b..01e981bdc 100644 --- a/packages/twenty-server/src/engine/integrations/event-emitter/types/object-record-delete.event.ts +++ b/packages/twenty-server/src/engine/integrations/event-emitter/types/object-record-delete.event.ts @@ -1,7 +1,7 @@ import { ObjectRecordBaseEvent } from 'src/engine/integrations/event-emitter/types/object-record.base.event'; export class ObjectRecordDeleteEvent extends ObjectRecordBaseEvent { - details: { + properties: { before: T; }; } diff --git a/packages/twenty-server/src/engine/integrations/event-emitter/types/object-record-job-data.ts b/packages/twenty-server/src/engine/integrations/event-emitter/types/object-record-job-data.ts new file mode 100644 index 000000000..d1f7aa5ec --- /dev/null +++ b/packages/twenty-server/src/engine/integrations/event-emitter/types/object-record-job-data.ts @@ -0,0 +1,11 @@ +import { ObjectRecordBaseEvent } from 'src/engine/integrations/event-emitter/types/object-record.base.event'; + +export class ObjectRecordJobData extends ObjectRecordBaseEvent { + getOperation() { + return this.name.split('.')[1]; + } + + getObjectName() { + return this.name.split('.')[0]; + } +} diff --git a/packages/twenty-server/src/engine/integrations/event-emitter/types/object-record-update.event.ts b/packages/twenty-server/src/engine/integrations/event-emitter/types/object-record-update.event.ts index 7f6dbfd04..037b38178 100644 --- a/packages/twenty-server/src/engine/integrations/event-emitter/types/object-record-update.event.ts +++ b/packages/twenty-server/src/engine/integrations/event-emitter/types/object-record-update.event.ts @@ -1,7 +1,7 @@ import { ObjectRecordBaseEvent } from 'src/engine/integrations/event-emitter/types/object-record.base.event'; export class ObjectRecordUpdateEvent extends ObjectRecordBaseEvent { - details: { + properties: { before: T; after: T; diff?: Partial; diff --git a/packages/twenty-server/src/engine/integrations/event-emitter/types/object-record.base.event.ts b/packages/twenty-server/src/engine/integrations/event-emitter/types/object-record.base.event.ts index a82ed3e0a..d34fbd1af 100644 --- a/packages/twenty-server/src/engine/integrations/event-emitter/types/object-record.base.event.ts +++ b/packages/twenty-server/src/engine/integrations/event-emitter/types/object-record.base.event.ts @@ -1,9 +1,11 @@ import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; export class ObjectRecordBaseEvent { + name: string; workspaceId: string; recordId: string; userId?: string; + workspaceMemberId?: string; objectMetadata: ObjectMetadataInterface; - details: any; + properties: any; } diff --git a/packages/twenty-server/src/engine/integrations/event-emitter/utils/__tests__/object-record-changed-values.spec.ts b/packages/twenty-server/src/engine/integrations/event-emitter/utils/__tests__/object-record-changed-values.spec.ts index abe8dad89..9da50ae4c 100644 --- a/packages/twenty-server/src/engine/integrations/event-emitter/utils/__tests__/object-record-changed-values.spec.ts +++ b/packages/twenty-server/src/engine/integrations/event-emitter/utils/__tests__/object-record-changed-values.spec.ts @@ -1,11 +1,35 @@ +import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; + import { objectRecordChangedValues } from 'src/engine/integrations/event-emitter/utils/object-record-changed-values'; +const mockObjectMetadata: ObjectMetadataInterface = { + id: '1', + nameSingular: 'Object', + namePlural: 'Objects', + labelSingular: 'Object', + labelPlural: 'Objects', + description: 'Test object metadata', + targetTableName: 'test_table', + fromRelations: [], + toRelations: [], + fields: [], + isSystem: false, + isCustom: false, + isActive: true, + isRemote: false, + isAuditLogged: true, +}; + describe('objectRecordChangedValues', () => { it('detects changes in scalar values correctly', () => { const oldRecord = { id: 1, name: 'Original Name', updatedAt: new Date() }; const newRecord = { id: 1, name: 'Updated Name', updatedAt: new Date() }; - const result = objectRecordChangedValues(oldRecord, newRecord); + const result = objectRecordChangedValues( + oldRecord, + newRecord, + mockObjectMetadata, + ); expect(result).toEqual({ name: { before: 'Original Name', after: 'Updated Name' }, @@ -13,20 +37,15 @@ describe('objectRecordChangedValues', () => { }); }); -it('ignores changes in properties that are objects', () => { - const oldRecord = { id: 1, details: { age: 20 } }; - const newRecord = { id: 1, details: { age: 21 } }; - - const result = objectRecordChangedValues(oldRecord, newRecord); - - expect(result).toEqual({}); -}); - it('ignores changes to the updatedAt field', () => { const oldRecord = { id: 1, updatedAt: new Date('2020-01-01') }; const newRecord = { id: 1, updatedAt: new Date('2024-01-01') }; - const result = objectRecordChangedValues(oldRecord, newRecord); + const result = objectRecordChangedValues( + oldRecord, + newRecord, + mockObjectMetadata, + ); expect(result).toEqual({}); }); @@ -35,7 +54,11 @@ it('returns an empty object when there are no changes', () => { const oldRecord = { id: 1, name: 'Name', value: 100 }; const newRecord = { id: 1, name: 'Name', value: 100 }; - const result = objectRecordChangedValues(oldRecord, newRecord); + const result = objectRecordChangedValues( + oldRecord, + newRecord, + mockObjectMetadata, + ); expect(result).toEqual({}); }); @@ -57,9 +80,14 @@ it('correctly handles a mix of changed, unchanged, and special case values', () }; const expectedChanges = { name: { before: 'Original', after: 'Updated' }, + config: { before: { theme: 'dark' }, after: { theme: 'light' } }, }; - const result = objectRecordChangedValues(oldRecord, newRecord); + const result = objectRecordChangedValues( + oldRecord, + newRecord, + mockObjectMetadata, + ); expect(result).toEqual(expectedChanges); }); diff --git a/packages/twenty-server/src/engine/integrations/event-emitter/utils/object-record-changed-values.ts b/packages/twenty-server/src/engine/integrations/event-emitter/utils/object-record-changed-values.ts index 99d82d2bf..ff300042d 100644 --- a/packages/twenty-server/src/engine/integrations/event-emitter/utils/object-record-changed-values.ts +++ b/packages/twenty-server/src/engine/integrations/event-emitter/utils/object-record-changed-values.ts @@ -1,21 +1,30 @@ import deepEqual from 'deep-equal'; +import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; + +import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; + export const objectRecordChangedValues = ( oldRecord: Record, newRecord: Record, + objectMetadata: ObjectMetadataInterface, ) => { - const isObject = (value: any) => { - return typeof value === 'object' && value !== null && !Array.isArray(value); - }; - const changedValues = Object.keys(newRecord).reduce( (acc, key) => { - // Discard if values are objects (e.g. we don't want Company.AccountOwner ; we have AccountOwnerId already) - if (isObject(oldRecord[key]) || isObject(newRecord[key])) { + if ( + objectMetadata.fields.find( + (field) => + field.type === FieldMetadataType.RELATION && field.name === key, + ) + ) { return acc; } - if (!deepEqual(oldRecord[key], newRecord[key]) && key != 'updatedAt') { + if (objectMetadata.nameSingular === 'activity' && key === 'body') { + return acc; + } + + if (!deepEqual(oldRecord[key], newRecord[key]) && key !== 'updatedAt') { acc[key] = { before: oldRecord[key], after: newRecord[key] }; } diff --git a/packages/twenty-server/src/engine/integrations/event-emitter/utils/object-record-diff-merge.ts b/packages/twenty-server/src/engine/integrations/event-emitter/utils/object-record-diff-merge.ts new file mode 100644 index 000000000..020ab9385 --- /dev/null +++ b/packages/twenty-server/src/engine/integrations/event-emitter/utils/object-record-diff-merge.ts @@ -0,0 +1,30 @@ +export function objectRecordDiffMerge( + oldRecord: Record, + newRecord: Record, +): Record { + const result: Record = { diff: {} }; + + // Iterate over the keys in the oldRecord diff + Object.keys(oldRecord.diff ?? {}).forEach((key) => { + if (newRecord.diff && newRecord.diff[key]) { + // If the key also exists in the newRecord, merge the 'before' from the oldRecord and the 'after' from the newRecord + result.diff[key] = { + before: oldRecord.diff[key].before, + after: newRecord.diff[key].after, + }; + } else { + // If the key does not exist in the newRecord, copy it as is from the oldRecord + result.diff[key] = oldRecord.diff[key]; + } + }); + + // Iterate over the keys in the newRecord diff to catch any that weren't in the oldRecord + Object.keys(newRecord.diff ?? {}).forEach((key) => { + if (!result.diff[key]) { + // If the key was not already added from the oldRecord, add it from the newRecord + result.diff[key] = newRecord.diff[key]; + } + }); + + return result; +} diff --git a/packages/twenty-server/src/engine/integrations/message-queue/jobs.module.ts b/packages/twenty-server/src/engine/integrations/message-queue/jobs.module.ts index 151e27ffd..5e694a5bb 100644 --- a/packages/twenty-server/src/engine/integrations/message-queue/jobs.module.ts +++ b/packages/twenty-server/src/engine/integrations/message-queue/jobs.module.ts @@ -9,8 +9,15 @@ import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { CallWebhookJobsJob } from 'src/engine/api/graphql/workspace-query-runner/jobs/call-webhook-jobs.job'; import { CallWebhookJob } from 'src/engine/api/graphql/workspace-query-runner/jobs/call-webhook.job'; import { RecordPositionBackfillJob } from 'src/engine/api/graphql/workspace-query-runner/jobs/record-position-backfill.job'; -import { SaveEventToDbJob } from 'src/engine/api/graphql/workspace-query-runner/jobs/save-event-to-db.job'; import { RecordPositionBackfillModule } from 'src/engine/api/graphql/workspace-query-runner/services/record-position-backfill-module'; +import { DeleteConnectedAccountAssociatedCalendarDataJob } from 'src/modules/calendar/jobs/delete-connected-account-associated-calendar-data.job'; +import { GoogleAPIRefreshAccessTokenModule } from 'src/modules/connected-account/services/google-api-refresh-access-token/google-api-refresh-access-token.module'; +import { MessageParticipantModule } from 'src/modules/messaging/services/message-participant/message-participant.module'; +import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; +import { ConnectedAccountObjectMetadata } from 'src/modules/connected-account/standard-objects/connected-account.object-metadata'; +import { MessageChannelObjectMetadata } from 'src/modules/messaging/standard-objects/message-channel.object-metadata'; +import { CreateCompanyAndContactJob } from 'src/modules/connected-account/auto-companies-and-contacts-creation/jobs/create-company-and-contact.job'; +import { AuditLogObjectMetadata } from 'src/modules/timeline/standard-objects/audit-log.object-metadata'; import { BillingModule } from 'src/engine/core-modules/billing/billing.module'; import { UpdateSubscriptionJob } from 'src/engine/core-modules/billing/jobs/update-subscription.job'; import { StripeModule } from 'src/engine/core-modules/billing/stripe/stripe.module'; @@ -24,24 +31,18 @@ import { EnvironmentModule } from 'src/engine/integrations/environment/environme import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; -import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { CleanInactiveWorkspaceJob } from 'src/engine/workspace-manager/workspace-cleaner/crons/clean-inactive-workspace.job'; import { MatchParticipantJob } from 'src/modules/calendar-messaging-participant/jobs/match-participant.job'; import { UnmatchParticipantJob } from 'src/modules/calendar-messaging-participant/jobs/unmatch-participant.job'; import { GoogleCalendarSyncCronJob } from 'src/modules/calendar/crons/jobs/google-calendar-sync.cron.job'; import { CalendarCreateCompanyAndContactAfterSyncJob } from 'src/modules/calendar/jobs/calendar-create-company-and-contact-after-sync.job'; -import { DeleteConnectedAccountAssociatedCalendarDataJob } from 'src/modules/calendar/jobs/delete-connected-account-associated-calendar-data.job'; import { GoogleCalendarSyncJob } from 'src/modules/calendar/jobs/google-calendar-sync.job'; import { CalendarEventCleanerModule } from 'src/modules/calendar/services/calendar-event-cleaner/calendar-event-cleaner.module'; import { CalendarEventParticipantModule } from 'src/modules/calendar/services/calendar-event-participant/calendar-event-participant.module'; import { GoogleCalendarSyncModule } from 'src/modules/calendar/services/google-calendar-sync/google-calendar-sync.module'; import { WorkspaceGoogleCalendarSyncModule } from 'src/modules/calendar/services/workspace-google-calendar-sync/workspace-google-calendar-sync.module'; import { AutoCompaniesAndContactsCreationModule } from 'src/modules/connected-account/auto-companies-and-contacts-creation/auto-companies-and-contacts-creation.module'; -import { CreateCompanyAndContactJob } from 'src/modules/connected-account/auto-companies-and-contacts-creation/jobs/create-company-and-contact.job'; -import { GoogleAPIRefreshAccessTokenModule } from 'src/modules/connected-account/services/google-api-refresh-access-token/google-api-refresh-access-token.module'; -import { ConnectedAccountObjectMetadata } from 'src/modules/connected-account/standard-objects/connected-account.object-metadata'; -import { EventObjectMetadata } from 'src/modules/event/standard-objects/event.object-metadata'; import { GmailFetchMessagesFromCacheCronJob } from 'src/modules/messaging/crons/jobs/gmail-fetch-messages-from-cache.cron.job'; import { GmailPartialSyncCronJob } from 'src/modules/messaging/crons/jobs/gmail-partial-sync.cron.job'; import { DeleteConnectedAccountAssociatedMessagingDataJob } from 'src/modules/messaging/jobs/delete-connected-account-associated-messaging-data.job'; @@ -50,12 +51,13 @@ import { GmailFullSyncJob } from 'src/modules/messaging/jobs/gmail-full-sync.job import { GmailPartialSyncJob } from 'src/modules/messaging/jobs/gmail-partial-sync.job'; import { MessagingCreateCompanyAndContactAfterSyncJob } from 'src/modules/messaging/jobs/messaging-create-company-and-contact-after-sync.job'; import { GmailFetchMessageContentFromCacheModule } from 'src/modules/messaging/services/gmail-fetch-message-content-from-cache/gmail-fetch-message-content-from-cache.module'; +import { CreateAuditLogFromInternalEvent } from 'src/modules/timeline/jobs/create-audit-log-from-internal-event'; +import { UpsertTimelineActivityFromInternalEvent } from 'src/modules/timeline/jobs/upsert-timeline-activity-from-internal-event.job'; import { GmailFullSyncModule } from 'src/modules/messaging/services/gmail-full-sync/gmail-full-sync.module'; import { GmailPartialSyncModule } from 'src/modules/messaging/services/gmail-partial-sync/gmail-partial-sync.module'; -import { MessageParticipantModule } from 'src/modules/messaging/services/message-participant/message-participant.module'; import { ThreadCleanerModule } from 'src/modules/messaging/services/thread-cleaner/thread-cleaner.module'; +import { TimelineActivityModule } from 'src/modules/timeline/timeline-activity.module'; import { MessageChannelMessageAssociationObjectMetadata } from 'src/modules/messaging/standard-objects/message-channel-message-association.object-metadata'; -import { MessageChannelObjectMetadata } from 'src/modules/messaging/standard-objects/message-channel.object-metadata'; @Module({ imports: [ @@ -83,13 +85,14 @@ import { MessageChannelObjectMetadata } from 'src/modules/messaging/standard-obj ObjectMetadataRepositoryModule.forFeature([ ConnectedAccountObjectMetadata, MessageChannelObjectMetadata, - EventObjectMetadata, + AuditLogObjectMetadata, MessageChannelMessageAssociationObjectMetadata, ]), GmailFullSyncModule, GmailFetchMessageContentFromCacheModule, GmailPartialSyncModule, CalendarEventParticipantModule, + TimelineActivityModule, ], providers: [ { @@ -156,8 +159,12 @@ import { MessageChannelObjectMetadata } from 'src/modules/messaging/standard-obj }, { - provide: SaveEventToDbJob.name, - useClass: SaveEventToDbJob, + provide: CreateAuditLogFromInternalEvent.name, + useClass: CreateAuditLogFromInternalEvent, + }, + { + provide: UpsertTimelineActivityFromInternalEvent.name, + useClass: UpsertTimelineActivityFromInternalEvent, }, { provide: GmailFetchMessagesFromCacheCronJob.name, diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts index 1f9f239ec..c83b72b3a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts @@ -44,8 +44,8 @@ import { attachmentStandardFieldIds, baseObjectStandardFieldIds, customObjectStandardFieldIds, - eventStandardFieldIds, favoriteStandardFieldIds, + timelineActivityStandardFieldIds, } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; import { createForeignKeyDeterministicUuid, @@ -475,10 +475,11 @@ export class ObjectMetadataService extends TypeOrmQueryService { - if (fieldMetadata.type === FieldMetadataType.RELATION) { - acc[fieldMetadata.objectMetadataId] = fieldMetadata; - } + const timelineActivityRelationFieldMetadataMap = + timelineActivityRelationFieldMetadata.reduce( + (acc, fieldMetadata: FieldMetadataEntity) => { + if (fieldMetadata.type === FieldMetadataType.RELATION) { + acc[fieldMetadata.objectMetadataId] = fieldMetadata; + } - return acc; - }, - {}, - ); + return acc; + }, + {}, + ); await this.relationMetadataRepository.save([ { workspaceId: workspaceId, relationType: RelationMetadataType.ONE_TO_MANY, fromObjectMetadataId: createdObjectMetadata.id, - toObjectMetadataId: eventObjectMetadata.id, + toObjectMetadataId: timelineActivityObjectMetadata.id, fromFieldMetadataId: - eventRelationFieldMetadataMap[createdObjectMetadata.id].id, + timelineActivityRelationFieldMetadataMap[createdObjectMetadata.id].id, toFieldMetadataId: - eventRelationFieldMetadataMap[eventObjectMetadata.id].id, + timelineActivityRelationFieldMetadataMap[ + timelineActivityObjectMetadata.id + ].id, onDeleteAction: RelationOnDeleteAction.CASCADE, }, ]); - return { eventObjectMetadata }; + return { timelineActivityObjectMetadata }; } private async createFavoriteRelation( diff --git a/packages/twenty-server/src/engine/object-metadata-repository/metadata-to-repository.mapping.ts b/packages/twenty-server/src/engine/object-metadata-repository/metadata-to-repository.mapping.ts index 33d7685e9..fc5c95188 100644 --- a/packages/twenty-server/src/engine/object-metadata-repository/metadata-to-repository.mapping.ts +++ b/packages/twenty-server/src/engine/object-metadata-repository/metadata-to-repository.mapping.ts @@ -5,7 +5,8 @@ import { CalendarEventRepository } from 'src/modules/calendar/repositories/calen import { CompanyRepository } from 'src/modules/company/repositories/company.repository'; import { BlocklistRepository } from 'src/modules/connected-account/repositories/blocklist.repository'; import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository'; -import { EventRepository } from 'src/modules/event/repositiories/event.repository'; +import { AuditLogRepository } from 'src/modules/timeline/repositiories/audit-log.repository'; +import { TimelineActivityRepository } from 'src/modules/timeline/repositiories/timeline-activity.repository'; import { MessageChannelMessageAssociationRepository } from 'src/modules/messaging/repositories/message-channel-message-association.repository'; import { MessageChannelRepository } from 'src/modules/messaging/repositories/message-channel.repository'; import { MessageParticipantRepository } from 'src/modules/messaging/repositories/message-participant.repository'; @@ -15,6 +16,7 @@ import { PersonRepository } from 'src/modules/person/repositories/person.reposit import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository'; export const metadataToRepositoryMapping = { + AuditLogObjectMetadata: AuditLogRepository, BlocklistObjectMetadata: BlocklistRepository, CalendarChannelEventAssociationObjectMetadata: CalendarChannelEventAssociationRepository, @@ -23,7 +25,6 @@ export const metadataToRepositoryMapping = { CalendarEventObjectMetadata: CalendarEventRepository, CompanyObjectMetadata: CompanyRepository, ConnectedAccountObjectMetadata: ConnectedAccountRepository, - EventObjectMetadata: EventRepository, MessageChannelMessageAssociationObjectMetadata: MessageChannelMessageAssociationRepository, MessageChannelObjectMetadata: MessageChannelRepository, @@ -31,5 +32,6 @@ export const metadataToRepositoryMapping = { MessageParticipantObjectMetadata: MessageParticipantRepository, MessageThreadObjectMetadata: MessageThreadRepository, PersonObjectMetadata: PersonRepository, + TimelineActivityObjectMetadata: TimelineActivityRepository, WorkspaceMemberObjectMetadata: WorkspaceMemberRepository, }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts index c9354a364..899aab966 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts @@ -122,7 +122,7 @@ export const companyStandardFieldIds = { opportunities: '20202020-add3-4658-8e23-d70dccb6d0ec', favorites: '20202020-4d1d-41ac-b13b-621631298d55', attachments: '20202020-c1b5-4120-b0f0-987ca401ed53', - events: '20202020-0414-4daf-9c0d-64fe7b27f89f', + timelineActivities: '72d5d7d3-8782-446c-a54b-1c25024f55db', }; export const connectedAccountStandardFieldIds = { @@ -146,6 +146,38 @@ export const eventStandardFieldIds = { custom: '20202020-4a71-41b0-9f83-9cdcca3f8b14', }; +export const auditLogStandardFieldIds = { + name: '20202020-2462-4b9d-b5d9-745febb3b095', + properties: '20202020-5d36-470e-8fad-d56ea3ab2fd0', + context: '20202020-b9d1-4058-9a75-7469cab5ca8c', + objectName: '20202020-76ba-4c47-b7e5-96034005d00a', + recordId: '20202020-c578-4acf-bf94-eb53b035cea2', + workspaceMember: '20202020-6e96-4300-b3f5-67a707147385', +}; + +export const behavioralEventStandardFieldIds = { + name: '20202020-2462-4b9d-b5d9-745febb3b095', + properties: '20202020-5d36-470e-8fad-d56ea3ab2fd0', + context: '20202020-bd62-4b5b-8385-6caeed8f8078', + objectName: '20202020-a744-406c-a2e1-9d83d74f4341', + recordId: '20202020-6d8b-4ca5-9869-f882cb335673', +}; + +export const timelineActivityStandardFieldIds = { + happensAt: '20202020-9526-4993-b339-c4318c4d39f0', + type: '20202020-5e7b-4ccd-8b8a-86b94b474134', + name: '20202020-7207-46e8-9dab-849505ae8497', + properties: '20202020-f142-4b04-b91b-6a2b4af3bf11', + workspaceMember: '20202020-af23-4479-9a30-868edc474b36', + person: '20202020-c414-45b9-a60a-ac27aa96229f', + company: '20202020-04ad-4221-a744-7a8278a5ce21', + opportunity: '20202020-7664-4a35-a3df-580d389fd527', + custom: '20202020-4a71-41b0-9f83-9cdcca3f8b15', + linkedRecordCachedName: '20202020-cfdb-4bef-bbce-a29f41230934', + linkedRecordId: '20202020-2e0e-48c0-b445-ee6c1e61687d', + linkedObjectMetadataId: '20202020-c595-449d-9f89-562758c9ee69', +}; + export const favoriteStandardFieldIds = { position: '20202020-dd26-42c6-8c3c-2a7598c204f6', workspaceMember: '20202020-ce63-49cb-9676-fdc0c45892cd', @@ -214,7 +246,7 @@ export const opportunityStandardFieldIds = { favorites: '20202020-a1c2-4500-aaae-83ba8a0e827a', activityTargets: '20202020-220a-42d6-8261-b2102d6eab35', attachments: '20202020-87c7-4118-83d6-2f4031005209', - events: '20202020-30e2-421f-96c7-19c69d1cf631', + timelineActivities: '863a6f5c-493a-47c8-9e14-34ed929d2ba6', }; export const personStandardFieldIds = { @@ -234,7 +266,7 @@ export const personStandardFieldIds = { attachments: '20202020-cd97-451f-87fa-bcb789bdbf3a', messageParticipants: '20202020-498e-4c61-8158-fa04f0638334', calendarEventParticipants: '20202020-52ee-45e9-a702-b64b3753e3a9', - events: '20202020-a43e-4873-9c23-e522de906ce5', + timelineActivities: 'f23d6471-78e0-458a-bdd0-9a84cd7d0b70', }; export const viewFieldStandardFieldIds = { @@ -295,7 +327,8 @@ export const workspaceMemberStandardFieldIds = { messageParticipants: '20202020-8f99-48bc-a5eb-edd33dd54188', blocklist: '20202020-6cb2-4161-9f29-a4b7f1283859', calendarEventParticipants: '20202020-0dbc-4841-9ce1-3e793b5b3512', - events: '20202020-e15b-47b8-94fe-8200e3c66615', + timelineActivities: '20202020-f0d9-4ba3-a123-69cc2c185071', + auditLogs: '20202020-2f54-4739-a5e2-99563385e83d', }; export const customObjectStandardFieldIds = { @@ -304,5 +337,5 @@ export const customObjectStandardFieldIds = { activityTargets: '20202020-7f42-40ae-b96c-c8a61acc83bf', favorites: '20202020-a4a7-4686-b296-1c6c3482ee21', attachments: '20202020-8d59-46ca-b7b2-73d167712134', - events: '20202020-a508-4334-9724-5c2bf1b05998', + timelineActivities: '20202020-f1ef-4ba4-8f33-1a4577afa477', }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids.ts index 7af744af5..9fb5b56bc 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids.ts @@ -11,6 +11,7 @@ export const standardObjectIds = { apiKey: '20202020-4c00-401d-8cda-ec6a4c41cd7d', attachment: '20202020-bd3d-4c60-8dca-571c71d4447a', blocklist: '20202020-0408-4f38-b8a8-4d5e3e26e24d', + behavioralEvent: '20202020-983d-416b-a5ee-bdd0da3d0f8f', calendarChannelEventAssociation: '20202020-491b-4aaa-9825-afd1bae6ae00', calendarChannel: '20202020-e8f2-40e1-a39c-c0e0039c5034', calendarEventParticipant: '20202020-a1c3-47a6-9732-27e5b1e8436d', @@ -18,8 +19,9 @@ export const standardObjectIds = { comment: '20202020-435f-4de9-89b5-97e32233bf5f', company: '20202020-b374-4779-a561-80086cb2e17f', connectedAccount: '20202020-977e-46b2-890b-c3002ddfd5c5', - event: '20202020-6736-4337-b5c4-8b39fae325a5', + event: '20202020-6736-4337-b5c4-8b39fae325a5', // Todo: remove favorite: '20202020-ab56-4e05-92a3-e2414a499860', + auditLog: '20202020-0566-476a-b4c4-a0f9781bd80a', messageChannelMessageAssociation: '20202020-ad1e-4127-bccb-d83ae04d2ccb', messageChannel: '20202020-fe8c-40bc-a681-b80b771449b7', messageParticipant: '20202020-a433-4456-aa2d-fd9cb26b774a', @@ -27,6 +29,7 @@ export const standardObjectIds = { message: '20202020-3f6b-4425-80ab-e468899ab4b2', opportunity: '20202020-9549-49dd-b2b2-883999db8938', person: '20202020-e674-48e5-a542-72570eee7213', + timelineActivity: '20202020-6736-4337-b5c4-8b39fae325a5', viewField: '20202020-4d19-4655-95bf-b2a04cf206d4', viewFilter: '20202020-6fb6-4631-aded-b7d67e952ec8', viewSort: '20202020-e46a-47a8-939a-e5d911f83531', diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/custom-objects/custom.object-metadata.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/custom-objects/custom.object-metadata.ts index c38fb3a95..299690c00 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/custom-objects/custom.object-metadata.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/custom-objects/custom.object-metadata.ts @@ -13,7 +13,7 @@ import { RelationMetadata } from 'src/engine/workspace-manager/workspace-sync-me import { FavoriteObjectMetadata } from 'src/modules/favorite/standard-objects/favorite.object-metadata'; import { AttachmentObjectMetadata } from 'src/modules/attachment/standard-objects/attachment.object-metadata'; import { customObjectStandardFieldIds } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; -import { EventObjectMetadata } from 'src/modules/event/standard-objects/event.object-metadata'; +import { TimelineActivityObjectMetadata } from 'src/modules/timeline/standard-objects/timeline-activity.object-metadata'; @BaseCustomObjectMetadata() export class CustomObjectMetadata extends BaseObjectMetadata { @@ -88,18 +88,20 @@ export class CustomObjectMetadata extends BaseObjectMetadata { attachments: AttachmentObjectMetadata[]; @FieldMetadata({ - standardId: customObjectStandardFieldIds.events, + standardId: customObjectStandardFieldIds.timelineActivities, type: FieldMetadataType.RELATION, - label: 'Events', + label: 'Timeline Activities', description: (objectMetadata) => - `Events tied to the ${objectMetadata.labelSingular}`, - icon: 'IconJson', + `Timeline Activities tied to the ${objectMetadata.labelSingular}`, + + icon: 'IconIconTimelineEvent', }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - inverseSideTarget: () => EventObjectMetadata, + inverseSideTarget: () => TimelineActivityObjectMetadata, onDelete: RelationOnDeleteAction.CASCADE, }) @IsNullable() - events: EventObjectMetadata[]; + @IsSystem() + timelineActivities: TimelineActivityObjectMetadata[]; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-objects/index.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-objects/index.ts index f0eba612c..f55e01609 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-objects/index.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-objects/index.ts @@ -24,21 +24,29 @@ import { ViewObjectMetadata } from 'src/modules/view/standard-objects/view.objec import { WebhookObjectMetadata } from 'src/modules/webhook/standard-objects/webhook.object-metadata'; import { WorkspaceMemberObjectMetadata } from 'src/modules/workspace-member/standard-objects/workspace-member.object-metadata'; import { CalendarChannelEventAssociationObjectMetadata } from 'src/modules/calendar/standard-objects/calendar-channel-event-association.object-metadata'; -import { EventObjectMetadata } from 'src/modules/event/standard-objects/event.object-metadata'; +import { AuditLogObjectMetadata } from 'src/modules/timeline/standard-objects/audit-log.object-metadata'; +import { TimelineActivityObjectMetadata } from 'src/modules/timeline/standard-objects/timeline-activity.object-metadata'; +import { BehavioralEventObjectMetadata } from 'src/modules/timeline/standard-objects/behavioral-event.object-metadata'; export const standardObjectMetadataDefinitions = [ ActivityTargetObjectMetadata, ActivityObjectMetadata, ApiKeyObjectMetadata, + AuditLogObjectMetadata, AttachmentObjectMetadata, + BehavioralEventObjectMetadata, BlocklistObjectMetadata, + CalendarEventObjectMetadata, + CalendarChannelObjectMetadata, + CalendarChannelEventAssociationObjectMetadata, + CalendarEventParticipantObjectMetadata, CommentObjectMetadata, CompanyObjectMetadata, ConnectedAccountObjectMetadata, - EventObjectMetadata, FavoriteObjectMetadata, OpportunityObjectMetadata, PersonObjectMetadata, + TimelineActivityObjectMetadata, ViewFieldObjectMetadata, ViewFilterObjectMetadata, ViewSortObjectMetadata, @@ -50,8 +58,4 @@ export const standardObjectMetadataDefinitions = [ MessageChannelObjectMetadata, MessageParticipantObjectMetadata, MessageChannelMessageAssociationObjectMetadata, - CalendarEventObjectMetadata, - CalendarChannelObjectMetadata, - CalendarChannelEventAssociationObjectMetadata, - CalendarEventParticipantObjectMetadata, ]; diff --git a/packages/twenty-server/src/modules/calendar-messaging-participant/listeners/participant-person.listener.ts b/packages/twenty-server/src/modules/calendar-messaging-participant/listeners/participant-person.listener.ts index 925d48997..52b8119fa 100644 --- a/packages/twenty-server/src/modules/calendar-messaging-participant/listeners/participant-person.listener.ts +++ b/packages/twenty-server/src/modules/calendar-messaging-participant/listeners/participant-person.listener.ts @@ -27,7 +27,7 @@ export class ParticipantPersonListener { async handleCreatedEvent( payload: ObjectRecordCreateEvent, ) { - if (payload.details.after.email === null) { + if (payload.properties.after.email === null) { return; } @@ -35,7 +35,7 @@ export class ParticipantPersonListener { MatchParticipantJob.name, { workspaceId: payload.workspaceId, - email: payload.details.after.email, + email: payload.properties.after.email, personId: payload.recordId, }, ); @@ -47,15 +47,15 @@ export class ParticipantPersonListener { ) { if ( objectRecordUpdateEventChangedProperties( - payload.details.before, - payload.details.after, + payload.properties.before, + payload.properties.after, ).includes('email') ) { await this.messageQueueService.add( UnmatchParticipantJob.name, { workspaceId: payload.workspaceId, - email: payload.details.before.email, + email: payload.properties.before.email, personId: payload.recordId, }, ); @@ -64,7 +64,7 @@ export class ParticipantPersonListener { MatchParticipantJob.name, { workspaceId: payload.workspaceId, - email: payload.details.after.email, + email: payload.properties.after.email, personId: payload.recordId, }, ); diff --git a/packages/twenty-server/src/modules/calendar-messaging-participant/listeners/participant-workspace-member.listener.ts b/packages/twenty-server/src/modules/calendar-messaging-participant/listeners/participant-workspace-member.listener.ts index fadb10b92..467764253 100644 --- a/packages/twenty-server/src/modules/calendar-messaging-participant/listeners/participant-workspace-member.listener.ts +++ b/packages/twenty-server/src/modules/calendar-messaging-participant/listeners/participant-workspace-member.listener.ts @@ -27,7 +27,7 @@ export class ParticipantWorkspaceMemberListener { async handleCreatedEvent( payload: ObjectRecordCreateEvent, ) { - if (payload.details.after.userEmail === null) { + if (payload.properties.after.userEmail === null) { return; } @@ -35,8 +35,8 @@ export class ParticipantWorkspaceMemberListener { MatchParticipantJob.name, { workspaceId: payload.workspaceId, - email: payload.details.after.userEmail, - workspaceMemberId: payload.details.after.id, + email: payload.properties.after.userEmail, + workspaceMemberId: payload.properties.after.id, }, ); } @@ -47,15 +47,15 @@ export class ParticipantWorkspaceMemberListener { ) { if ( objectRecordUpdateEventChangedProperties( - payload.details.before, - payload.details.after, + payload.properties.before, + payload.properties.after, ).includes('userEmail') ) { await this.messageQueueService.add( UnmatchParticipantJob.name, { workspaceId: payload.workspaceId, - email: payload.details.before.userEmail, + email: payload.properties.before.userEmail, personId: payload.recordId, }, ); @@ -64,7 +64,7 @@ export class ParticipantWorkspaceMemberListener { MatchParticipantJob.name, { workspaceId: payload.workspaceId, - email: payload.details.after.userEmail, + email: payload.properties.after.userEmail, workspaceMemberId: payload.recordId, }, ); diff --git a/packages/twenty-server/src/modules/calendar/listeners/calendar-channel.listener.ts b/packages/twenty-server/src/modules/calendar/listeners/calendar-channel.listener.ts index 5d1ee4e21..bfa9f8973 100644 --- a/packages/twenty-server/src/modules/calendar/listeners/calendar-channel.listener.ts +++ b/packages/twenty-server/src/modules/calendar/listeners/calendar-channel.listener.ts @@ -24,10 +24,10 @@ export class CalendarChannelListener { ) { if ( objectRecordChangedProperties( - payload.details.before, - payload.details.after, + payload.properties.before, + payload.properties.after, ).includes('isContactAutoCreationEnabled') && - payload.details.after.isContactAutoCreationEnabled + payload.properties.after.isContactAutoCreationEnabled ) { await this.messageQueueService.add( CalendarCreateCompanyAndContactAfterSyncJob.name, diff --git a/packages/twenty-server/src/modules/company/standard-objects/company.object-metadata.ts b/packages/twenty-server/src/modules/company/standard-objects/company.object-metadata.ts index e4a2ea05f..faff56862 100644 --- a/packages/twenty-server/src/modules/company/standard-objects/company.object-metadata.ts +++ b/packages/twenty-server/src/modules/company/standard-objects/company.object-metadata.ts @@ -21,7 +21,7 @@ import { FavoriteObjectMetadata } from 'src/modules/favorite/standard-objects/fa import { OpportunityObjectMetadata } from 'src/modules/opportunity/standard-objects/opportunity.object-metadata'; import { PersonObjectMetadata } from 'src/modules/person/standard-objects/person.object-metadata'; import { WorkspaceMemberObjectMetadata } from 'src/modules/workspace-member/standard-objects/workspace-member.object-metadata'; -import { EventObjectMetadata } from 'src/modules/event/standard-objects/event.object-metadata'; +import { TimelineActivityObjectMetadata } from 'src/modules/timeline/standard-objects/timeline-activity.object-metadata'; @ObjectMetadata({ standardId: standardObjectIds.company, @@ -213,18 +213,18 @@ export class CompanyObjectMetadata extends BaseObjectMetadata { attachments: Relation; @FieldMetadata({ - standardId: companyStandardFieldIds.events, + standardId: companyStandardFieldIds.timelineActivities, type: FieldMetadataType.RELATION, - label: 'Events', - description: 'Events linked to the company', + label: 'Timeline Activities', + description: 'Timeline Activities linked to the company', icon: 'IconIconTimelineEvent', }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - inverseSideTarget: () => EventObjectMetadata, + inverseSideTarget: () => TimelineActivityObjectMetadata, onDelete: RelationOnDeleteAction.CASCADE, }) @IsNullable() @IsSystem() - events: Relation; + timelineActivities: Relation; } diff --git a/packages/twenty-server/src/modules/event/standard-objects/event.object-metadata.ts b/packages/twenty-server/src/modules/event/standard-objects/event.object-metadata.ts index cf3dd809e..ed9f611dd 100644 --- a/packages/twenty-server/src/modules/event/standard-objects/event.object-metadata.ts +++ b/packages/twenty-server/src/modules/event/standard-objects/event.object-metadata.ts @@ -16,6 +16,9 @@ import { OpportunityObjectMetadata } from 'src/modules/opportunity/standard-obje import { PersonObjectMetadata } from 'src/modules/person/standard-objects/person.object-metadata'; import { WorkspaceMemberObjectMetadata } from 'src/modules/workspace-member/standard-objects/workspace-member.object-metadata'; +// TODO: Depricate +// This should be removed in the next release +// We use AuditLog and ActivityTimeline instead @ObjectMetadata({ standardId: standardObjectIds.event, namePlural: 'events', diff --git a/packages/twenty-server/src/modules/messaging/listeners/messaging-message-channel.listener.ts b/packages/twenty-server/src/modules/messaging/listeners/messaging-message-channel.listener.ts index 0af01d281..8c53167bb 100644 --- a/packages/twenty-server/src/modules/messaging/listeners/messaging-message-channel.listener.ts +++ b/packages/twenty-server/src/modules/messaging/listeners/messaging-message-channel.listener.ts @@ -24,10 +24,10 @@ export class MessagingMessageChannelListener { ) { if ( objectRecordChangedProperties( - payload.details.before, - payload.details.after, + payload.properties.before, + payload.properties.after, ).includes('isContactAutoCreationEnabled') && - payload.details.after.isContactAutoCreationEnabled + payload.properties.after.isContactAutoCreationEnabled ) { await this.messageQueueService.add( MessagingCreateCompanyAndContactAfterSyncJob.name, diff --git a/packages/twenty-server/src/modules/opportunity/standard-objects/opportunity.object-metadata.ts b/packages/twenty-server/src/modules/opportunity/standard-objects/opportunity.object-metadata.ts index 0d16e0680..cebedb83f 100644 --- a/packages/twenty-server/src/modules/opportunity/standard-objects/opportunity.object-metadata.ts +++ b/packages/twenty-server/src/modules/opportunity/standard-objects/opportunity.object-metadata.ts @@ -19,7 +19,7 @@ import { BaseObjectMetadata } from 'src/engine/workspace-manager/workspace-sync- import { CompanyObjectMetadata } from 'src/modules/company/standard-objects/company.object-metadata'; import { FavoriteObjectMetadata } from 'src/modules/favorite/standard-objects/favorite.object-metadata'; import { PersonObjectMetadata } from 'src/modules/person/standard-objects/person.object-metadata'; -import { EventObjectMetadata } from 'src/modules/event/standard-objects/event.object-metadata'; +import { TimelineActivityObjectMetadata } from 'src/modules/timeline/standard-objects/timeline-activity.object-metadata'; import { IsNotAuditLogged } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/is-not-audit-logged.decorator'; @ObjectMetadata({ @@ -173,17 +173,17 @@ export class OpportunityObjectMetadata extends BaseObjectMetadata { attachments: Relation; @FieldMetadata({ - standardId: opportunityStandardFieldIds.events, + standardId: opportunityStandardFieldIds.timelineActivities, type: FieldMetadataType.RELATION, - label: 'Events', - description: 'Events linked to the opportunity.', + label: 'Timeline Activities', + description: 'Timeline Activities linked to the opportunity.', icon: 'IconTimelineEvent', }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - inverseSideTarget: () => EventObjectMetadata, + inverseSideTarget: () => TimelineActivityObjectMetadata, onDelete: RelationOnDeleteAction.SET_NULL, }) @IsNullable() - events: Relation; + timelineActivities: Relation; } diff --git a/packages/twenty-server/src/modules/person/standard-objects/person.object-metadata.ts b/packages/twenty-server/src/modules/person/standard-objects/person.object-metadata.ts index 14ff30468..33c9fad19 100644 --- a/packages/twenty-server/src/modules/person/standard-objects/person.object-metadata.ts +++ b/packages/twenty-server/src/modules/person/standard-objects/person.object-metadata.ts @@ -23,7 +23,7 @@ import { CompanyObjectMetadata } from 'src/modules/company/standard-objects/comp import { FavoriteObjectMetadata } from 'src/modules/favorite/standard-objects/favorite.object-metadata'; import { MessageParticipantObjectMetadata } from 'src/modules/messaging/standard-objects/message-participant.object-metadata'; import { OpportunityObjectMetadata } from 'src/modules/opportunity/standard-objects/opportunity.object-metadata'; -import { EventObjectMetadata } from 'src/modules/event/standard-objects/event.object-metadata'; +import { TimelineActivityObjectMetadata } from 'src/modules/timeline/standard-objects/timeline-activity.object-metadata'; @ObjectMetadata({ standardId: standardObjectIds.person, @@ -226,7 +226,7 @@ export class PersonObjectMetadata extends BaseObjectMetadata { calendarEventParticipants: Relation; @FieldMetadata({ - standardId: personStandardFieldIds.events, + standardId: personStandardFieldIds.timelineActivities, type: FieldMetadataType.RELATION, label: 'Events', description: 'Events linked to the company', @@ -234,10 +234,10 @@ export class PersonObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - inverseSideTarget: () => EventObjectMetadata, + inverseSideTarget: () => TimelineActivityObjectMetadata, onDelete: RelationOnDeleteAction.CASCADE, }) @IsNullable() @IsSystem() - events: Relation; + timelineActivities: Relation; } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/save-event-to-db.job.ts b/packages/twenty-server/src/modules/timeline/jobs/create-audit-log-from-internal-event.ts similarity index 56% rename from packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/save-event-to-db.job.ts rename to packages/twenty-server/src/modules/timeline/jobs/create-audit-log-from-internal-event.ts index 7810ae2f8..32d4ca269 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/save-event-to-db.job.ts +++ b/packages/twenty-server/src/modules/timeline/jobs/create-audit-log-from-internal-event.ts @@ -2,32 +2,25 @@ import { Injectable } from '@nestjs/common'; import { MessageQueueJob } from 'src/engine/integrations/message-queue/interfaces/message-queue-job.interface'; +import { ObjectRecordBaseEvent } from 'src/engine/integrations/event-emitter/types/object-record.base.event'; import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; -import { EventRepository } from 'src/modules/event/repositiories/event.repository'; -import { EventObjectMetadata } from 'src/modules/event/standard-objects/event.object-metadata'; +import { AuditLogRepository } from 'src/modules/timeline/repositiories/audit-log.repository'; +import { AuditLogObjectMetadata } from 'src/modules/timeline/standard-objects/audit-log.object-metadata'; import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository'; import { WorkspaceMemberObjectMetadata } from 'src/modules/workspace-member/standard-objects/workspace-member.object-metadata'; -export type SaveEventToDbJobData = { - workspaceId: string; - recordId: string; - userId: string | undefined; - objectName: string; - operation: string; - details: any; -}; - @Injectable() -export class SaveEventToDbJob implements MessageQueueJob { +export class CreateAuditLogFromInternalEvent + implements MessageQueueJob +{ constructor( @InjectObjectMetadataRepository(WorkspaceMemberObjectMetadata) private readonly workspaceMemberService: WorkspaceMemberRepository, - @InjectObjectMetadataRepository(EventObjectMetadata) - private readonly eventService: EventRepository, + @InjectObjectMetadataRepository(AuditLogObjectMetadata) + private readonly auditLogRepository: AuditLogRepository, ) {} - // TODO: need to support objects others than "person", "company", "opportunity" - async handle(data: SaveEventToDbJobData): Promise { + async handle(data: ObjectRecordBaseEvent): Promise { let workspaceMemberId: string | null = null; if (data.userId) { @@ -39,18 +32,19 @@ export class SaveEventToDbJob implements MessageQueueJob { workspaceMemberId = workspaceMember.id; } - if (data.details.diff) { + if (data.properties.diff) { // we remove "before" and "after" property for a cleaner/slimmer event payload - data.details = { - diff: data.details.diff, + data.properties = { + diff: data.properties.diff, }; } - await this.eventService.insert( - `${data.operation}.${data.objectName}`, - data.details, + await this.auditLogRepository.insert( + data.name, + data.properties, workspaceMemberId, - data.objectName, + data.name.split('.')[0], + data.objectMetadata.id, data.recordId, data.workspaceId, ); diff --git a/packages/twenty-server/src/modules/timeline/jobs/upsert-timeline-activity-from-behavioral-event.ts b/packages/twenty-server/src/modules/timeline/jobs/upsert-timeline-activity-from-behavioral-event.ts new file mode 100644 index 000000000..70b786d12 --- /dev/null +++ b/packages/twenty-server/src/modules/timeline/jobs/upsert-timeline-activity-from-behavioral-event.ts @@ -0,0 +1 @@ +// TODO diff --git a/packages/twenty-server/src/modules/timeline/jobs/upsert-timeline-activity-from-internal-event.job.ts b/packages/twenty-server/src/modules/timeline/jobs/upsert-timeline-activity-from-internal-event.job.ts new file mode 100644 index 000000000..36410bb4e --- /dev/null +++ b/packages/twenty-server/src/modules/timeline/jobs/upsert-timeline-activity-from-internal-event.job.ts @@ -0,0 +1,50 @@ +import { Injectable } from '@nestjs/common'; + +import { MessageQueueJob } from 'src/engine/integrations/message-queue/interfaces/message-queue-job.interface'; + +import { ObjectRecordBaseEvent } from 'src/engine/integrations/event-emitter/types/object-record.base.event'; +import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; +import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository'; +import { WorkspaceMemberObjectMetadata } from 'src/modules/workspace-member/standard-objects/workspace-member.object-metadata'; +import { TimelineActivityService } from 'src/modules/timeline/services/timeline-activity.service'; + +@Injectable() +export class UpsertTimelineActivityFromInternalEvent + implements MessageQueueJob +{ + constructor( + @InjectObjectMetadataRepository(WorkspaceMemberObjectMetadata) + private readonly workspaceMemberService: WorkspaceMemberRepository, + private readonly timelineActivityService: TimelineActivityService, + ) {} + + async handle(data: ObjectRecordBaseEvent): Promise { + if (data.userId) { + const workspaceMember = await this.workspaceMemberService.getByIdOrFail( + data.userId, + data.workspaceId, + ); + + data.workspaceMemberId = workspaceMember.id; + } + + if (data.properties.diff) { + // we remove "before" and "after" property for a cleaner/slimmer event payload + data.properties = { + diff: data.properties.diff, + }; + } + + // Temporary + // We ignore every that is not a LinkedObject or a Business Object + if ( + data.objectMetadata.isSystem && + data.objectMetadata.nameSingular !== 'activityTarget' && + data.objectMetadata.nameSingular !== 'activity' + ) { + return; + } + + await this.timelineActivityService.upsertEvent(data); + } +} diff --git a/packages/twenty-server/src/modules/event/repositiories/event.repository.ts b/packages/twenty-server/src/modules/timeline/repositiories/audit-log.repository.ts similarity index 60% rename from packages/twenty-server/src/modules/event/repositiories/event.repository.ts rename to packages/twenty-server/src/modules/timeline/repositiories/audit-log.repository.ts index 1da98ee59..001ecb751 100644 --- a/packages/twenty-server/src/modules/event/repositiories/event.repository.ts +++ b/packages/twenty-server/src/modules/timeline/repositiories/audit-log.repository.ts @@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common'; import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; @Injectable() -export class EventRepository { +export class AuditLogRepository { constructor( private readonly workspaceDataSourceService: WorkspaceDataSourceService, ) {} @@ -13,17 +13,25 @@ export class EventRepository { properties: string, workspaceMemberId: string | null, objectName: string, - objectId: string, + objectMetadataId: string, + recordId: string, workspaceId: string, ): Promise { const dataSourceSchema = this.workspaceDataSourceService.getSchemaName(workspaceId); await this.workspaceDataSourceService.executeRawQuery( - `INSERT INTO ${dataSourceSchema}."event" - ("name", "properties", "workspaceMemberId", "${objectName}Id") - VALUES ($1, $2, $3, $4)`, - [name, properties, workspaceMemberId, objectId], + `INSERT INTO ${dataSourceSchema}."auditLog" + ("name", "properties", "workspaceMemberId", "objectName", "objectMetadataId", "recordId") + VALUES ($1, $2, $3, $4, $5, $6)`, + [ + name, + properties, + workspaceMemberId, + objectName, + objectMetadataId, + recordId, + ], workspaceId, ); } diff --git a/packages/twenty-server/src/modules/timeline/repositiories/timeline-activity.repository.ts b/packages/twenty-server/src/modules/timeline/repositiories/timeline-activity.repository.ts new file mode 100644 index 000000000..0c88db351 --- /dev/null +++ b/packages/twenty-server/src/modules/timeline/repositiories/timeline-activity.repository.ts @@ -0,0 +1,136 @@ +import { Injectable } from '@nestjs/common'; + +import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; +import { objectRecordDiffMerge } from 'src/engine/integrations/event-emitter/utils/object-record-diff-merge'; + +@Injectable() +export class TimelineActivityRepository { + constructor( + private readonly workspaceDataSourceService: WorkspaceDataSourceService, + ) {} + + async upsertOne( + name: string, + properties: Record, + objectName: string, + recordId: string, + workspaceId: string, + workspaceMemberId?: string, + linkedRecordCachedName?: string, + linkedRecordId?: string, + linkedObjectMetadataId?: string, + ) { + const dataSourceSchema = + this.workspaceDataSourceService.getSchemaName(workspaceId); + + const recentTimelineActivity = await this.findRecentTimelineActivity( + dataSourceSchema, + name, + objectName, + recordId, + workspaceMemberId, + linkedRecordId, + workspaceId, + ); + + if (recentTimelineActivity.length !== 0) { + const newProps = objectRecordDiffMerge( + recentTimelineActivity[0].properties, + properties, + ); + + return this.updateTimelineActivity( + dataSourceSchema, + recentTimelineActivity[0].id, + newProps, + workspaceMemberId, + workspaceId, + ); + } + + return this.insertTimelineActivity( + dataSourceSchema, + name, + properties, + objectName, + recordId, + workspaceMemberId, + linkedRecordCachedName ?? '', + linkedRecordId, + linkedObjectMetadataId, + workspaceId, + ); + } + + private async findRecentTimelineActivity( + dataSourceSchema: string, + name: string, + objectName: string, + recordId: string, + workspaceMemberId: string | undefined, + linkedRecordId: string | undefined, + workspaceId: string, + ) { + return this.workspaceDataSourceService.executeRawQuery( + `SELECT * FROM ${dataSourceSchema}."timelineActivity" + WHERE "${objectName}Id" = $1 + AND ("name" = $2 OR "name" = $3) + AND "workspaceMemberId" = $4 + AND "linkedRecordId" = $5 + AND "createdAt" >= NOW() - interval '10 minutes'`, + [ + recordId, + name, + name.replace(/\.updated$/, '.created'), + workspaceMemberId, + linkedRecordId, + ], + workspaceId, + ); + } + + private async updateTimelineActivity( + dataSourceSchema: string, + id: string, + properties: Record, + workspaceMemberId: string | undefined, + workspaceId: string, + ) { + return this.workspaceDataSourceService.executeRawQuery( + `UPDATE ${dataSourceSchema}."timelineActivity" + SET "properties" = $2, "workspaceMemberId" = $3 + WHERE "id" = $1`, + [id, properties, workspaceMemberId], + workspaceId, + ); + } + + private async insertTimelineActivity( + dataSourceSchema: string, + name: string, + properties: Record, + objectName: string, + recordId: string, + workspaceMemberId: string | undefined, + linkedRecordCachedName: string, + linkedRecordId: string | undefined, + linkedObjectMetadataId: string | undefined, + workspaceId: string, + ) { + return this.workspaceDataSourceService.executeRawQuery( + `INSERT INTO ${dataSourceSchema}."timelineActivity" + ("name", "properties", "workspaceMemberId", "${objectName}Id", "linkedRecordCachedName", "linkedRecordId", "linkedObjectMetadataId") + VALUES ($1, $2, $3, $4, $5, $6, $7)`, + [ + name, + properties, + workspaceMemberId, + recordId, + linkedRecordCachedName ?? '', + linkedRecordId, + linkedObjectMetadataId, + ], + workspaceId, + ); + } +} diff --git a/packages/twenty-server/src/modules/timeline/services/timeline-activity.service.ts b/packages/twenty-server/src/modules/timeline/services/timeline-activity.service.ts new file mode 100644 index 000000000..74117c362 --- /dev/null +++ b/packages/twenty-server/src/modules/timeline/services/timeline-activity.service.ts @@ -0,0 +1,173 @@ +import { Injectable } from '@nestjs/common'; + +import { ObjectRecordBaseEvent } from 'src/engine/integrations/event-emitter/types/object-record.base.event'; +import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; +import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; +import { TimelineActivityRepository } from 'src/modules/timeline/repositiories/timeline-activity.repository'; +import { TimelineActivityObjectMetadata } from 'src/modules/timeline/standard-objects/timeline-activity.object-metadata'; + +type TransformedEvent = ObjectRecordBaseEvent & { + objectName?: string; + linkedRecordCachedName?: string; + linkedRecordId?: string; + linkedObjectMetadataId?: string; +}; + +@Injectable() +export class TimelineActivityService { + constructor( + @InjectObjectMetadataRepository(TimelineActivityObjectMetadata) + private readonly timelineActivityRepository: TimelineActivityRepository, + private readonly workspaceDataSourceService: WorkspaceDataSourceService, + ) {} + + async upsertEvent(event: ObjectRecordBaseEvent) { + const events = await this.transformEvent(event); + + if (!events || events.length === 0) return; + + for (const event of events) { + return await this.timelineActivityRepository.upsertOne( + event.name, + event.properties, + event.objectName ?? event.objectMetadata.nameSingular, + event.recordId, + event.workspaceId, + event.workspaceMemberId, + event.linkedRecordCachedName, + event.linkedRecordId, + event.linkedObjectMetadataId, + ); + } + } + + private async transformEvent( + event: ObjectRecordBaseEvent, + ): Promise { + if ( + ['activity', 'messageParticipant', 'activityTarget'].includes( + event.objectMetadata.nameSingular, + ) + ) { + return await this.handleLinkedObjects(event); + } + + return [event]; + } + + private async handleLinkedObjects(event: ObjectRecordBaseEvent) { + const dataSourceSchema = this.workspaceDataSourceService.getSchemaName( + event.workspaceId, + ); + + switch (event.objectMetadata.nameSingular) { + case 'activityTarget': + return this.processActivityTarget(event, dataSourceSchema); + case 'activity': + return this.processActivity(event, dataSourceSchema); + default: + return []; + } + } + + private async processActivity( + event: ObjectRecordBaseEvent, + dataSourceSchema: string, + ) { + const activityTargets = + await this.workspaceDataSourceService.executeRawQuery( + `SELECT * FROM ${dataSourceSchema}."activityTarget" + WHERE "activityId" = $1`, + [event.recordId], + event.workspaceId, + ); + + const activity = await this.workspaceDataSourceService.executeRawQuery( + `SELECT * FROM ${dataSourceSchema}."activity" + WHERE "id" = $1`, + [event.recordId], + event.workspaceId, + ); + + if (activityTargets.length === 0) return; + if (activity.length === 0) return; + + return activityTargets + .map((activityTarget) => { + const targetColumn: string[] = Object.entries(activityTarget) + .map(([columnName, columnValue]: [string, string]) => { + if (columnName === 'activityId' || !columnName.endsWith('Id')) + return; + if (columnValue === null) return; + + return columnName; + }) + .filter((column): column is string => column !== undefined); + + if (targetColumn.length === 0) return; + + return { + ...event, + name: activity[0].type.toLowerCase() + '.' + event.name.split('.')[1], + objectName: targetColumn[0].replace(/Id$/, ''), + recordId: activityTarget[targetColumn[0]], + linkedRecordCachedName: activity[0].title, + linkedRecordId: activity[0].id, + linkedObjectMetadataId: event.objectMetadata.id, + } as TransformedEvent; + }) + .filter((event): event is TransformedEvent => event !== undefined); + } + + private async processActivityTarget( + event: ObjectRecordBaseEvent, + dataSourceSchema: string, + ) { + const activityTarget = + await this.workspaceDataSourceService.executeRawQuery( + `SELECT * FROM ${dataSourceSchema}."activityTarget" + WHERE "id" = $1`, + [event.recordId], + event.workspaceId, + ); + + if (activityTarget.length === 0) return; + + const activity = await this.workspaceDataSourceService.executeRawQuery( + `SELECT * FROM ${dataSourceSchema}."activity" + WHERE "id" = $1`, + [activityTarget[0].activityId], + event.workspaceId, + ); + + if (activity.length === 0) return; + + const activityObjectMetadataId = event.objectMetadata.fields.find( + (field) => field.name === 'activity', + )?.toRelationMetadata?.fromObjectMetadataId; + + const targetColumn: string[] = Object.entries(activityTarget[0]) + .map(([columnName, columnValue]: [string, string]) => { + if (columnName === 'activityId' || !columnName.endsWith('Id')) return; + if (columnValue === null) return; + + return columnName; + }) + .filter((column): column is string => column !== undefined); + + if (targetColumn.length === 0) return; + + return [ + { + ...event, + name: activity[0].type.toLowerCase() + '.' + event.name.split('.')[1], + properties: {}, + objectName: targetColumn[0].replace(/Id$/, ''), + recordId: activityTarget[0][targetColumn[0]], + linkedRecordCachedName: activity[0].title, + linkedRecordId: activity[0].id, + linkedObjectMetadataId: activityObjectMetadataId, + }, + ] as TransformedEvent[]; + } +} diff --git a/packages/twenty-server/src/modules/timeline/standard-objects/audit-log.object-metadata.ts b/packages/twenty-server/src/modules/timeline/standard-objects/audit-log.object-metadata.ts new file mode 100644 index 000000000..7adda5dfd --- /dev/null +++ b/packages/twenty-server/src/modules/timeline/standard-objects/audit-log.object-metadata.ts @@ -0,0 +1,96 @@ +import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; + +import { FeatureFlagKeys } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; +import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { auditLogStandardFieldIds } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; +import { standardObjectIds } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; +import { FieldMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/field-metadata.decorator'; +import { Gate } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/gate.decorator'; +import { IsNullable } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/is-nullable.decorator'; +import { IsSystem } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/is-system.decorator'; +import { ObjectMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/object-metadata.decorator'; +import { BaseObjectMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects/base.object-metadata'; +import { WorkspaceMemberObjectMetadata } from 'src/modules/workspace-member/standard-objects/workspace-member.object-metadata'; + +@ObjectMetadata({ + standardId: standardObjectIds.auditLog, + namePlural: 'auditLogs', + labelSingular: 'Audit Log', + labelPlural: 'Audit Logs', + description: 'An audit log of actions performed in the system', + icon: 'IconIconTimelineEvent', +}) +@IsSystem() +@Gate({ + featureFlag: FeatureFlagKeys.IsEventObjectEnabled, +}) +export class AuditLogObjectMetadata extends BaseObjectMetadata { + @FieldMetadata({ + standardId: auditLogStandardFieldIds.name, + type: FieldMetadataType.TEXT, + label: 'Event name', + description: 'Event name/type', + icon: 'IconAbc', + }) + name: string; + + @FieldMetadata({ + standardId: auditLogStandardFieldIds.properties, + type: FieldMetadataType.RAW_JSON, + label: 'Event details', + description: 'Json value for event details', + icon: 'IconListDetails', + }) + @IsNullable() + properties: JSON; + + @FieldMetadata({ + standardId: auditLogStandardFieldIds.context, + type: FieldMetadataType.RAW_JSON, + label: 'Event context', + description: + 'Json object to provide context (user, device, workspace, etc.)', + icon: 'IconListDetails', + }) + @IsNullable() + context: JSON; + + @FieldMetadata({ + standardId: auditLogStandardFieldIds.objectName, + type: FieldMetadataType.TEXT, + label: 'Object name', + description: 'If the event is related to a particular object', + icon: 'IconAbc', + }) + objectName: string; + + @FieldMetadata({ + standardId: auditLogStandardFieldIds.objectName, + type: FieldMetadataType.TEXT, + label: 'Object name', + description: 'If the event is related to a particular object', + icon: 'IconAbc', + }) + objectMetadataId: string; + + @FieldMetadata({ + standardId: auditLogStandardFieldIds.recordId, + type: FieldMetadataType.UUID, + label: 'Object id', + description: 'Event name/type', + icon: 'IconAbc', + }) + @IsNullable() + recordId: string; + + @FieldMetadata({ + standardId: auditLogStandardFieldIds.workspaceMember, + type: FieldMetadataType.RELATION, + label: 'Workspace Member', + description: 'Event workspace member', + icon: 'IconCircleUser', + joinColumn: 'workspaceMemberId', + }) + @IsNullable() + workspaceMember: Relation; +} diff --git a/packages/twenty-server/src/modules/timeline/standard-objects/behavioral-event.object-metadata.ts b/packages/twenty-server/src/modules/timeline/standard-objects/behavioral-event.object-metadata.ts new file mode 100644 index 000000000..b39938886 --- /dev/null +++ b/packages/twenty-server/src/modules/timeline/standard-objects/behavioral-event.object-metadata.ts @@ -0,0 +1,90 @@ +import { FeatureFlagKeys } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; +import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { behavioralEventStandardFieldIds } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; +import { standardObjectIds } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; +import { FieldMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/field-metadata.decorator'; +import { Gate } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/gate.decorator'; +import { IsNullable } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/is-nullable.decorator'; +import { IsSystem } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/is-system.decorator'; +import { ObjectMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/object-metadata.decorator'; +import { BaseObjectMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects/base.object-metadata'; + +@ObjectMetadata({ + standardId: standardObjectIds.behavioralEvent, + namePlural: 'behavioralEvents', + labelSingular: 'Behavioral Event', + labelPlural: 'Behavioral Events', + description: 'An event related to user behavior', + icon: 'IconIconTimelineEvent', +}) +@IsSystem() +@Gate({ + featureFlag: FeatureFlagKeys.IsEventObjectEnabled, +}) +export class BehavioralEventObjectMetadata extends BaseObjectMetadata { + /** + * + * Common in Segment, Rudderstack, etc. + * = Track, Screen, Page... + * But doesn't feel that useful. + * Let's try living without it. + * + @FieldMetadata({ + standardId: behavioralEventStandardFieldIds.type, + type: FieldMetadataType.TEXT, + label: 'Event type', + description: 'Event type', + icon: 'IconAbc', + }) + type: string; + */ + + @FieldMetadata({ + standardId: behavioralEventStandardFieldIds.name, + type: FieldMetadataType.TEXT, + label: 'Event name', + description: 'Event name', + icon: 'IconAbc', + }) + name: string; + + @FieldMetadata({ + standardId: behavioralEventStandardFieldIds.properties, + type: FieldMetadataType.RAW_JSON, + label: 'Event details', + description: 'Json value for event details', + icon: 'IconListDetails', + }) + @IsNullable() + properties: JSON; + + @FieldMetadata({ + standardId: behavioralEventStandardFieldIds.context, + type: FieldMetadataType.RAW_JSON, + label: 'Event context', + description: + 'Json object to provide context (user, device, workspace, etc.)', + icon: 'IconListDetails', + }) + @IsNullable() + context: JSON; + + @FieldMetadata({ + standardId: behavioralEventStandardFieldIds.objectName, + type: FieldMetadataType.TEXT, + label: 'Object name', + description: 'If the event is related to a particular object', + icon: 'IconAbc', + }) + objectName: string; + + @FieldMetadata({ + standardId: behavioralEventStandardFieldIds.recordId, + type: FieldMetadataType.UUID, + label: 'Object id', + description: 'Event name/type', + icon: 'IconAbc', + }) + @IsNullable() + recordId: string; +} diff --git a/packages/twenty-server/src/modules/timeline/standard-objects/timeline-activity.object-metadata.ts b/packages/twenty-server/src/modules/timeline/standard-objects/timeline-activity.object-metadata.ts new file mode 100644 index 000000000..5532615e7 --- /dev/null +++ b/packages/twenty-server/src/modules/timeline/standard-objects/timeline-activity.object-metadata.ts @@ -0,0 +1,148 @@ +import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; + +import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { timelineActivityStandardFieldIds } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; +import { standardObjectIds } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; +import { DynamicRelationFieldMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/dynamic-field-metadata.interface'; +import { FieldMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/field-metadata.decorator'; +import { IsNotAuditLogged } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/is-not-audit-logged.decorator'; +import { IsNullable } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/is-nullable.decorator'; +import { IsSystem } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/is-system.decorator'; +import { ObjectMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/object-metadata.decorator'; +import { BaseObjectMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects/base.object-metadata'; +import { CompanyObjectMetadata } from 'src/modules/company/standard-objects/company.object-metadata'; +import { OpportunityObjectMetadata } from 'src/modules/opportunity/standard-objects/opportunity.object-metadata'; +import { PersonObjectMetadata } from 'src/modules/person/standard-objects/person.object-metadata'; +import { WorkspaceMemberObjectMetadata } from 'src/modules/workspace-member/standard-objects/workspace-member.object-metadata'; +import { FeatureFlagKeys } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; +import { Gate } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/gate.decorator'; +import { CustomObjectMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/custom-objects/custom.object-metadata'; + +@ObjectMetadata({ + standardId: standardObjectIds.timelineActivity, + namePlural: 'timelineActivities', + labelSingular: 'Timeline Activity', + labelPlural: 'Timeline Activities', + description: 'Aggregated / filtered event to be displayed on the timeline', + icon: 'IconIconTimelineEvent', +}) +@IsSystem() +@IsNotAuditLogged() +@Gate({ + featureFlag: FeatureFlagKeys.IsEventObjectEnabled, +}) +export class TimelineActivityObjectMetadata extends BaseObjectMetadata { + @FieldMetadata({ + standardId: timelineActivityStandardFieldIds.happensAt, + type: FieldMetadataType.DATE_TIME, + label: 'Creation date', + description: 'Creation date', + icon: 'IconCalendar', + defaultValue: 'now', + }) + happensAt: Date; + + @FieldMetadata({ + standardId: timelineActivityStandardFieldIds.name, + type: FieldMetadataType.TEXT, + label: 'Event name', + description: 'Event name', + icon: 'IconAbc', + }) + name: string; + + @FieldMetadata({ + standardId: timelineActivityStandardFieldIds.properties, + type: FieldMetadataType.RAW_JSON, + label: 'Event details', + description: 'Json value for event details', + icon: 'IconListDetails', + }) + @IsNullable() + properties: JSON; + + // Who made the action + @FieldMetadata({ + standardId: timelineActivityStandardFieldIds.workspaceMember, + type: FieldMetadataType.RELATION, + label: 'Workspace Member', + description: 'Event workspace member', + icon: 'IconCircleUser', + joinColumn: 'workspaceMemberId', + }) + @IsNullable() + workspaceMember: Relation; + + @FieldMetadata({ + standardId: timelineActivityStandardFieldIds.person, + type: FieldMetadataType.RELATION, + label: 'Person', + description: 'Event person', + icon: 'IconUser', + joinColumn: 'personId', + }) + @IsNullable() + person: Relation; + + @FieldMetadata({ + standardId: timelineActivityStandardFieldIds.company, + type: FieldMetadataType.RELATION, + label: 'Company', + description: 'Event company', + icon: 'IconBuildingSkyscraper', + joinColumn: 'companyId', + }) + @IsNullable() + company: Relation; + + @FieldMetadata({ + standardId: timelineActivityStandardFieldIds.opportunity, + type: FieldMetadataType.RELATION, + label: 'Opportunity', + description: 'Events opportunity', + icon: 'IconTargetArrow', + joinColumn: 'opportunityId', + }) + @IsNullable() + opportunity: Relation; + + @DynamicRelationFieldMetadata((oppositeObjectMetadata) => ({ + standardId: timelineActivityStandardFieldIds.custom, + name: oppositeObjectMetadata.nameSingular, + label: oppositeObjectMetadata.labelSingular, + description: `Event ${oppositeObjectMetadata.labelSingular}`, + joinColumn: `${oppositeObjectMetadata.nameSingular}Id`, + icon: 'IconTimeline', + })) + custom: Relation; + + // Special objects that don't have their own timeline and are 'link' to the main object + @FieldMetadata({ + standardId: timelineActivityStandardFieldIds.linkedRecordCachedName, + type: FieldMetadataType.TEXT, + label: 'Linked Record cached name', + description: 'Cached record name', + icon: 'IconAbc', + }) + linkedRecordCachedName: string; + + @FieldMetadata({ + standardId: timelineActivityStandardFieldIds.linkedRecordId, + type: FieldMetadataType.UUID, + label: 'Linked Record id', + description: 'Linked Record id', + icon: 'IconAbc', + }) + @IsNullable() + linkedRecordId: string; + + @FieldMetadata({ + standardId: timelineActivityStandardFieldIds.linkedObjectMetadataId, + type: FieldMetadataType.UUID, + label: 'Linked Object Metadata Id', + description: 'inked Object Metadata Id', + icon: 'IconAbc', + }) + @IsNullable() + linkedObjectMetadataId: string; +} diff --git a/packages/twenty-server/src/modules/timeline/timeline-activity.module.ts b/packages/twenty-server/src/modules/timeline/timeline-activity.module.ts new file mode 100644 index 000000000..e7b044d27 --- /dev/null +++ b/packages/twenty-server/src/modules/timeline/timeline-activity.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; + +import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; +import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; +import { TimelineActivityService } from 'src/modules/timeline/services/timeline-activity.service'; +import { TimelineActivityObjectMetadata } from 'src/modules/timeline/standard-objects/timeline-activity.object-metadata'; + +@Module({ + imports: [ + WorkspaceDataSourceModule, + ObjectMetadataRepositoryModule.forFeature([TimelineActivityObjectMetadata]), + ], + providers: [TimelineActivityService], + exports: [TimelineActivityService], +}) +export class TimelineActivityModule {} diff --git a/packages/twenty-server/src/modules/workspace-member/standard-objects/workspace-member.object-metadata.ts b/packages/twenty-server/src/modules/workspace-member/standard-objects/workspace-member.object-metadata.ts index 002c00605..bbb976c85 100644 --- a/packages/twenty-server/src/modules/workspace-member/standard-objects/workspace-member.object-metadata.ts +++ b/packages/twenty-server/src/modules/workspace-member/standard-objects/workspace-member.object-metadata.ts @@ -24,7 +24,8 @@ import { ConnectedAccountObjectMetadata } from 'src/modules/connected-account/st import { FavoriteObjectMetadata } from 'src/modules/favorite/standard-objects/favorite.object-metadata'; import { MessageParticipantObjectMetadata } from 'src/modules/messaging/standard-objects/message-participant.object-metadata'; import { IsNullable } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/is-nullable.decorator'; -import { EventObjectMetadata } from 'src/modules/event/standard-objects/event.object-metadata'; +import { TimelineActivityObjectMetadata } from 'src/modules/timeline/standard-objects/timeline-activity.object-metadata'; +import { AuditLogObjectMetadata } from 'src/modules/timeline/standard-objects/audit-log.object-metadata'; import { IsNotAuditLogged } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/is-not-audit-logged.decorator'; @ObjectMetadata({ @@ -248,7 +249,7 @@ export class WorkspaceMemberObjectMetadata extends BaseObjectMetadata { calendarEventParticipants: Relation; @FieldMetadata({ - standardId: workspaceMemberStandardFieldIds.events, + standardId: workspaceMemberStandardFieldIds.timelineActivities, type: FieldMetadataType.RELATION, label: 'Events', description: 'Events linked to the workspace member', @@ -256,10 +257,26 @@ export class WorkspaceMemberObjectMetadata extends BaseObjectMetadata { }) @RelationMetadata({ type: RelationMetadataType.ONE_TO_MANY, - inverseSideTarget: () => EventObjectMetadata, + inverseSideTarget: () => TimelineActivityObjectMetadata, onDelete: RelationOnDeleteAction.CASCADE, }) @IsNullable() @IsSystem() - events: Relation; + timelineActivities: Relation; + + @FieldMetadata({ + standardId: workspaceMemberStandardFieldIds.auditLogs, + type: FieldMetadataType.RELATION, + label: 'Aud tLogs', + description: 'Audit Logs linked to the workspace member', + icon: 'IconTimelineEvent', + }) + @RelationMetadata({ + type: RelationMetadataType.ONE_TO_MANY, + inverseSideTarget: () => AuditLogObjectMetadata, + onDelete: RelationOnDeleteAction.SET_NULL, + }) + @IsNullable() + @IsSystem() + auditLogs: Relation; }