From d876b400566ff53f9fe8282e64b25fbb051e60aa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?F=C3=A9lix=20Malfait?=
Date: Fri, 22 Mar 2024 14:01:16 +0100
Subject: [PATCH] Logs show page (#4611)
* Being implementing events on the frontend
* Rename JSON to RAW JSON
* Fix handling of json field on frontend
* Log user id
* Add frontend tests
* Update packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/save-event-to-db.job.ts
Co-authored-by: Weiko
* Move db calls to a dedicated repository
* Add server-side tests
---------
Co-authored-by: Weiko
---
.../src/generated-metadata/graphql.ts | 67 +++++++++++++-
.../twenty-front/src/generated/graphql.tsx | 46 +---------
.../events/components/EventList.tsx | 22 +++++
.../activities/events/components/EventRow.tsx | 11 +++
.../activities/events/components/Events.tsx | 25 +++++
.../events/hooks/__tests__/useEvents.test.ts | 91 +++++++++++++++++++
.../activities/events/hooks/useEvents.tsx | 28 ++++++
.../modules/activities/events/types/Event.ts | 12 +++
.../types/CoreObjectNameSingular.ts | 1 +
.../utils/mapFieldMetadataToGraphQLQuery.ts | 1 +
.../record-field/types/FieldMetadata.ts | 5 +
.../record-field/types/FieldType.ts | 3 +-
.../types/guards/assertFieldMetadata.ts | 5 +-
.../types/guards/isFieldRawJson.ts | 6 ++
.../components/ShowPageRightContainer.tsx | 12 ++-
.../jobs/save-event-to-db.job.ts | 54 ++++++-----
.../listeners/entity-events-to-db.listener.ts | 10 +-
.../workspace-query-runner.module.ts | 7 ++
.../workspace-query-runner.service.ts | 12 ++-
.../graphql-types/input/index.ts | 2 +-
...-type.ts => raw-json-filter.input-type.ts} | 4 +-
.../services/type-mapper.service.ts | 8 +-
.../open-api/utils/components.utils.ts | 2 +-
.../types/object-record-update.event.ts | 1 +
.../types/object-record.base.event.ts | 1 +
.../object-record-changed-values.spec.ts | 65 +++++++++++++
.../utils/object-record-changed-values.ts | 28 ++++++
.../dtos/default-value.input.ts | 2 +-
.../field-metadata/field-metadata.entity.ts | 2 +-
.../field-metadata-default-value.interface.ts | 4 +-
.../utils/generate-target-column-map.util.ts | 2 +-
.../validate-default-value-for-type.util.ts | 4 +-
...field-metadata-type-to-column-type.util.ts | 2 +-
.../workspace-migration.factory.ts | 2 +-
.../metadata-to-repository.mapping.ts | 2 +
...p-field-metadata-type-to-data-type.util.ts | 2 +-
.../event/repositiories/event.repository.ts | 30 ++++++
.../standard-objects/event.object-metadata.ts | 2 +-
38 files changed, 488 insertions(+), 95 deletions(-)
create mode 100644 packages/twenty-front/src/modules/activities/events/components/EventList.tsx
create mode 100644 packages/twenty-front/src/modules/activities/events/components/EventRow.tsx
create mode 100644 packages/twenty-front/src/modules/activities/events/components/Events.tsx
create mode 100644 packages/twenty-front/src/modules/activities/events/hooks/__tests__/useEvents.test.ts
create mode 100644 packages/twenty-front/src/modules/activities/events/hooks/useEvents.tsx
create mode 100644 packages/twenty-front/src/modules/activities/events/types/Event.ts
create mode 100644 packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRawJson.ts
rename packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/{json-filter.input-type.ts => raw-json-filter.input-type.ts} (71%)
create mode 100644 packages/twenty-server/src/engine/integrations/event-emitter/utils/__tests__/object-record-changed-values.spec.ts
create mode 100644 packages/twenty-server/src/engine/integrations/event-emitter/utils/object-record-changed-values.ts
create mode 100644 packages/twenty-server/src/modules/event/repositiories/event.repository.ts
diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts
index 63d820235..927fde138 100644
--- a/packages/twenty-front/src/generated-metadata/graphql.ts
+++ b/packages/twenty-front/src/generated-metadata/graphql.ts
@@ -73,6 +73,7 @@ export type Billing = {
export type BillingSubscription = {
__typename?: 'BillingSubscription';
id: Scalars['ID']['output'];
+ interval?: Maybe;
status: Scalars['String']['output'];
};
@@ -263,7 +264,6 @@ export enum FieldMetadataType {
DateTime = 'DATE_TIME',
Email = 'EMAIL',
FullName = 'FULL_NAME',
- Json = 'JSON',
Link = 'LINK',
MultiSelect = 'MULTI_SELECT',
Number = 'NUMBER',
@@ -272,6 +272,7 @@ export enum FieldMetadataType {
Position = 'POSITION',
Probability = 'PROBABILITY',
Rating = 'RATING',
+ RawJson = 'RAW_JSON',
Relation = 'RELATION',
Select = 'SELECT',
Text = 'TEXT',
@@ -341,6 +342,7 @@ export type Mutation = {
renewToken: AuthTokens;
signUp: LoginToken;
track: Analytics;
+ updateBillingSubscription: UpdateBillingEntity;
updateOneField: Field;
updateOneObject: Object;
updatePasswordViaResetToken: InvalidatePassword;
@@ -545,6 +547,8 @@ export type Query = {
fields: FieldConnection;
findWorkspaceFromInviteHash: Workspace;
getProductPrices: ProductPricesEntity;
+ getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal;
+ getTimelineCalendarEventsFromPersonId: TimelineCalendarEventsWithTotal;
getTimelineThreadsFromCompanyId: TimelineThreadsWithTotal;
getTimelineThreadsFromPersonId: TimelineThreadsWithTotal;
object: Object;
@@ -591,6 +595,20 @@ export type QueryGetProductPricesArgs = {
};
+export type QueryGetTimelineCalendarEventsFromCompanyIdArgs = {
+ companyId: Scalars['ID']['input'];
+ page: Scalars['Int']['input'];
+ pageSize: Scalars['Int']['input'];
+};
+
+
+export type QueryGetTimelineCalendarEventsFromPersonIdArgs = {
+ page: Scalars['Int']['input'];
+ pageSize: Scalars['Int']['input'];
+ personId: Scalars['ID']['input'];
+};
+
+
export type QueryGetTimelineThreadsFromCompanyIdArgs = {
companyId: Scalars['ID']['input'];
page: Scalars['Int']['input'];
@@ -697,7 +715,7 @@ export type Sentry = {
export type SessionEntity = {
__typename?: 'SessionEntity';
- url: Scalars['String']['output'];
+ url?: Maybe;
};
/** Sort Directions */
@@ -724,6 +742,45 @@ export type Telemetry = {
enabled: Scalars['Boolean']['output'];
};
+export type TimelineCalendarEvent = {
+ __typename?: 'TimelineCalendarEvent';
+ attendees: Array;
+ conferenceSolution: Scalars['String']['output'];
+ conferenceUri: Scalars['String']['output'];
+ description: Scalars['String']['output'];
+ endsAt: Scalars['DateTime']['output'];
+ id: Scalars['ID']['output'];
+ isCanceled: Scalars['Boolean']['output'];
+ isFullDay: Scalars['Boolean']['output'];
+ location: Scalars['String']['output'];
+ startsAt: Scalars['DateTime']['output'];
+ title: Scalars['String']['output'];
+ visibility: TimelineCalendarEventVisibility;
+};
+
+export type TimelineCalendarEventAttendee = {
+ __typename?: 'TimelineCalendarEventAttendee';
+ avatarUrl: Scalars['String']['output'];
+ displayName: Scalars['String']['output'];
+ firstName: Scalars['String']['output'];
+ handle: Scalars['String']['output'];
+ lastName: Scalars['String']['output'];
+ personId?: Maybe;
+ workspaceMemberId?: Maybe;
+};
+
+/** Visibility of the calendar event */
+export enum TimelineCalendarEventVisibility {
+ Metadata = 'METADATA',
+ ShareEverything = 'SHARE_EVERYTHING'
+}
+
+export type TimelineCalendarEventsWithTotal = {
+ __typename?: 'TimelineCalendarEventsWithTotal';
+ timelineCalendarEvents: Array;
+ totalNumberOfCalendarEvents: Scalars['Int']['output'];
+};
+
export type TimelineThread = {
__typename?: 'TimelineThread';
firstParticipant: TimelineThreadParticipant;
@@ -760,6 +817,12 @@ export type TransientToken = {
transientToken: AuthToken;
};
+export type UpdateBillingEntity = {
+ __typename?: 'UpdateBillingEntity';
+ /** Boolean that confirms query was successful */
+ success: Scalars['Boolean']['output'];
+};
+
export type UpdateFieldInput = {
defaultValue?: InputMaybe;
description?: InputMaybe;
diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx
index 73da02df1..3a576f6b2 100644
--- a/packages/twenty-front/src/generated/graphql.tsx
+++ b/packages/twenty-front/src/generated/graphql.tsx
@@ -184,7 +184,6 @@ export enum FieldMetadataType {
DateTime = 'DATE_TIME',
Email = 'EMAIL',
FullName = 'FULL_NAME',
- Json = 'JSON',
Link = 'LINK',
MultiSelect = 'MULTI_SELECT',
Number = 'NUMBER',
@@ -193,6 +192,7 @@ export enum FieldMetadataType {
Position = 'POSITION',
Probability = 'PROBABILITY',
Rating = 'RATING',
+ RawJson = 'RAW_JSON',
Relation = 'RELATION',
Select = 'SELECT',
Text = 'TEXT',
@@ -255,7 +255,6 @@ export type Mutation = {
generateJWT: AuthTokens;
generateTransientToken: TransientToken;
impersonate: Verify;
- removeWorkspaceMember: Scalars['String'];
renewToken: AuthTokens;
signUp: LoginToken;
track: Analytics;
@@ -314,11 +313,6 @@ export type MutationImpersonateArgs = {
};
-export type MutationRemoveWorkspaceMemberArgs = {
- memberId: Scalars['String'];
-};
-
-
export type MutationRenewTokenArgs = {
refreshToken: Scalars['String'];
};
@@ -1098,13 +1092,6 @@ export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', status: string, interval?: string | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null } | null }> } };
-export type RemoveWorkspaceMemberMutationVariables = Exact<{
- memberId: Scalars['String'];
-}>;
-
-
-export type RemoveWorkspaceMemberMutation = { __typename?: 'Mutation', removeWorkspaceMember: string };
-
export type ActivateWorkspaceMutationVariables = Exact<{
input: ActivateWorkspaceInput;
}>;
@@ -2311,37 +2298,6 @@ export function useGetCurrentUserLazyQuery(baseOptions?: Apollo.LazyQueryHookOpt
export type GetCurrentUserQueryHookResult = ReturnType;
export type GetCurrentUserLazyQueryHookResult = ReturnType;
export type GetCurrentUserQueryResult = Apollo.QueryResult;
-export const RemoveWorkspaceMemberDocument = gql`
- mutation RemoveWorkspaceMember($memberId: String!) {
- removeWorkspaceMember(memberId: $memberId)
-}
- `;
-export type RemoveWorkspaceMemberMutationFn = Apollo.MutationFunction;
-
-/**
- * __useRemoveWorkspaceMemberMutation__
- *
- * To run a mutation, you first call `useRemoveWorkspaceMemberMutation` within a React component and pass it any options that fit your needs.
- * When your component renders, `useRemoveWorkspaceMemberMutation` returns a tuple that includes:
- * - A mutate function that you can call at any time to execute the mutation
- * - An object with fields that represent the current status of the mutation's execution
- *
- * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
- *
- * @example
- * const [removeWorkspaceMemberMutation, { data, loading, error }] = useRemoveWorkspaceMemberMutation({
- * variables: {
- * memberId: // value for 'memberId'
- * },
- * });
- */
-export function useRemoveWorkspaceMemberMutation(baseOptions?: Apollo.MutationHookOptions) {
- const options = {...defaultOptions, ...baseOptions}
- return Apollo.useMutation(RemoveWorkspaceMemberDocument, options);
- }
-export type RemoveWorkspaceMemberMutationHookResult = ReturnType;
-export type RemoveWorkspaceMemberMutationResult = Apollo.MutationResult;
-export type RemoveWorkspaceMemberMutationOptions = Apollo.BaseMutationOptions;
export const ActivateWorkspaceDocument = gql`
mutation ActivateWorkspace($input: ActivateWorkspaceInput!) {
activateWorkspace(data: $input) {
diff --git a/packages/twenty-front/src/modules/activities/events/components/EventList.tsx b/packages/twenty-front/src/modules/activities/events/components/EventList.tsx
new file mode 100644
index 000000000..158e3ba12
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/events/components/EventList.tsx
@@ -0,0 +1,22 @@
+import { ReactElement } from 'react';
+
+import { EventRow } from '@/activities/events/components/EventRow';
+import { Event } from '@/activities/events/types/Event';
+import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
+
+type EventListProps = {
+ targetableObject: ActivityTargetableObject;
+ title: string;
+ events: Event[];
+ button?: ReactElement | false;
+};
+
+export const EventList = ({ events }: EventListProps) => {
+ return (
+ <>
+ {events &&
+ events.length > 0 &&
+ events.map((event: Event) => )}
+ >
+ );
+};
diff --git a/packages/twenty-front/src/modules/activities/events/components/EventRow.tsx b/packages/twenty-front/src/modules/activities/events/components/EventRow.tsx
new file mode 100644
index 000000000..d7bbc2d75
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/events/components/EventRow.tsx
@@ -0,0 +1,11 @@
+import { Event } from '@/activities/events/types/Event';
+
+export const EventRow = ({ event }: { event: Event }) => {
+ return (
+ <>
+
+ {event.name}:
{event.properties}
+
+ >
+ );
+};
diff --git a/packages/twenty-front/src/modules/activities/events/components/Events.tsx b/packages/twenty-front/src/modules/activities/events/components/Events.tsx
new file mode 100644
index 000000000..11536d79a
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/events/components/Events.tsx
@@ -0,0 +1,25 @@
+import { isNonEmptyArray } from '@sniptt/guards';
+
+import { EventList } from '@/activities/events/components/EventList';
+import { useEvents } from '@/activities/events/hooks/useEvents';
+import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
+
+export const Events = ({
+ targetableObject,
+}: {
+ targetableObject: ActivityTargetableObject;
+}) => {
+ const { events } = useEvents(targetableObject);
+
+ if (!isNonEmptyArray(events)) {
+ return No log yet
;
+ }
+
+ return (
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/activities/events/hooks/__tests__/useEvents.test.ts b/packages/twenty-front/src/modules/activities/events/hooks/__tests__/useEvents.test.ts
new file mode 100644
index 000000000..25c6d5bf1
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/events/hooks/__tests__/useEvents.test.ts
@@ -0,0 +1,91 @@
+import { renderHook } from '@testing-library/react';
+
+import { useEvents } from '@/activities/events/hooks/useEvents';
+
+jest.mock('@/object-record/hooks/useFindManyRecords', () => ({
+ useFindManyRecords: jest.fn(),
+}));
+
+describe('useEvent', () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('fetches events correctly for a given targetableObject', () => {
+ const mockEvents = [
+ {
+ __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',
+ avatarUrl: '',
+ updatedAt: '2024-03-21T16:01:41.839Z',
+ name: {
+ __typename: 'FullName',
+ firstName: 'Tim',
+ lastName: 'Apple',
+ },
+ id: '20202020-0687-4c41-b707-ed1bfca972a7',
+ userEmail: 'tim@apple.dev',
+ colorScheme: 'Light',
+ createdAt: '2024-03-21T16:01:41.839Z',
+ userId: '20202020-9e3b-46d4-a556-88b9ddc2b034',
+ },
+ workspaceMemberId: '20202020-0687-4c41-b707-ed1bfca972a7',
+ createdAt: '2024-03-22T08:28:44.830Z',
+ name: 'updated.company',
+ companyId: '460b6fb1-ed89-413a-b31a-962986e67bb4',
+ properties: '{"diff": {"address": {"after": "Paris", "before": ""}}}',
+ updatedAt: '2024-03-22T08:28:44.830Z',
+ },
+ ];
+ const mockTargetableObject = {
+ id: '1',
+ targetObjectNameSingular: 'Opportunity',
+ };
+
+ const useFindManyRecordsMock = jest.requireMock(
+ '@/object-record/hooks/useFindManyRecords',
+ );
+ useFindManyRecordsMock.useFindManyRecords.mockReturnValue({
+ records: mockEvents,
+ });
+
+ const { result } = renderHook(() => useEvents(mockTargetableObject));
+
+ expect(result.current.events).toEqual(mockEvents);
+ });
+});
diff --git a/packages/twenty-front/src/modules/activities/events/hooks/useEvents.tsx b/packages/twenty-front/src/modules/activities/events/hooks/useEvents.tsx
new file mode 100644
index 000000000..8e37947a6
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/events/hooks/useEvents.tsx
@@ -0,0 +1,28 @@
+import { Event } from '@/activities/events/types/Event';
+import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
+import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
+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) => {
+ const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({
+ nameSingular: targetableObject.targetObjectNameSingular,
+ });
+
+ const { records: events } = useFindManyRecords({
+ objectNameSingular: CoreObjectNameSingular.Event,
+ filter: {
+ [targetableObjectFieldIdName]: {
+ eq: targetableObject.id,
+ },
+ },
+ orderBy: {
+ createdAt: 'DescNullsFirst',
+ },
+ });
+
+ return {
+ events: events as Event[],
+ };
+};
diff --git a/packages/twenty-front/src/modules/activities/events/types/Event.ts b/packages/twenty-front/src/modules/activities/events/types/Event.ts
new file mode 100644
index 000000000..1752b8367
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/events/types/Event.ts
@@ -0,0 +1,12 @@
+export type Event = {
+ id: string;
+ createdAt: string;
+ updatedAt: string;
+ deletedAt: string | null;
+ opportunityId: string | null;
+ companyId: string;
+ personId: string;
+ workspaceMemberId: string;
+ properties: any;
+ name: string;
+};
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 10602b7e3..9886f09b7 100644
--- a/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts
+++ b/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts
@@ -8,6 +8,7 @@ export enum CoreObjectNameSingular {
Comment = 'comment',
Company = 'company',
ConnectedAccount = 'connectedAccount',
+ Event = 'event',
Favorite = 'favorite',
Message = 'message',
MessageChannel = 'messageChannel',
diff --git a/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts b/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts
index 4603ac0ce..e6dc29a1c 100644
--- a/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts
+++ b/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts
@@ -34,6 +34,7 @@ export const mapFieldMetadataToGraphQLQuery = ({
'RATING',
'SELECT',
'POSITION',
+ 'RAW_JSON',
] as FieldMetadataType[]
).includes(fieldType);
diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts b/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts
index 74f8bbe88..7f2281983 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts
+++ b/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts
@@ -69,6 +69,11 @@ export type FieldRatingMetadata = {
fieldName: string;
};
+export type FieldRawJsonMetadata = {
+ objectMetadataNameSingular?: string;
+ fieldName: string;
+};
+
export type FieldDefinitionRelationType =
| 'FROM_MANY_OBJECTS'
| 'FROM_ONE_OBJECT'
diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/FieldType.ts b/packages/twenty-front/src/modules/object-record/record-field/types/FieldType.ts
index e09af18f8..d59a6bb84 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/types/FieldType.ts
+++ b/packages/twenty-front/src/modules/object-record/record-field/types/FieldType.ts
@@ -17,4 +17,5 @@ export type FieldType =
| 'URL'
| 'UUID'
| 'MULTI_SELECT'
- | 'NUMERIC';
+ | 'NUMERIC'
+ | 'RAW_JSON';
diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/guards/assertFieldMetadata.ts b/packages/twenty-front/src/modules/object-record/record-field/types/guards/assertFieldMetadata.ts
index 86f1922cb..0cf44a152 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/types/guards/assertFieldMetadata.ts
+++ b/packages/twenty-front/src/modules/object-record/record-field/types/guards/assertFieldMetadata.ts
@@ -10,6 +10,7 @@ import {
FieldNumberMetadata,
FieldPhoneMetadata,
FieldRatingMetadata,
+ FieldRawJsonMetadata,
FieldRelationMetadata,
FieldSelectMetadata,
FieldTextMetadata,
@@ -47,7 +48,9 @@ type AssertFieldMetadataFunction = <
? FieldTextMetadata
: E extends 'UUID'
? FieldUuidMetadata
- : never,
+ : E extends 'RAW_JSON'
+ ? FieldRawJsonMetadata
+ : never,
>(
fieldType: E,
fieldTypeGuard: (
diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRawJson.ts b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRawJson.ts
new file mode 100644
index 000000000..3decadfb8
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRawJson.ts
@@ -0,0 +1,6 @@
+import { FieldDefinition } from '../FieldDefinition';
+import { FieldMetadata, FieldRawJsonMetadata } from '../FieldMetadata';
+
+export const isFieldRawJson = (
+ field: Pick, 'type'>,
+): field is FieldDefinition => field.type === 'RAW_JSON';
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 390e5e9e1..001904181 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
@@ -3,6 +3,7 @@ import { useRecoilValue } from 'recoil';
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';
@@ -65,6 +66,8 @@ export const ShowPageRightContainer = ({
const activeTabId = useRecoilValue(activeTabIdState);
const shouldDisplayCalendarTab = useIsFeatureEnabled('IS_CALENDAR_ENABLED');
+ const shouldDisplayLogTab = useIsFeatureEnabled('IS_EVENT_OBJECT_ENABLED');
+
const shouldDisplayEmailsTab =
(emails &&
targetableObject.targetObjectNameSingular ===
@@ -101,7 +104,6 @@ export const ShowPageRightContainer = ({
title: 'Emails',
Icon: IconMail,
hide: !shouldDisplayEmailsTab,
- hasBetaPill: true,
},
{
id: 'calendar',
@@ -109,6 +111,13 @@ export const ShowPageRightContainer = ({
Icon: IconCalendarEvent,
hide: !shouldDisplayCalendarTab,
},
+ {
+ id: 'logs',
+ title: 'Logs',
+ Icon: IconTimelineEvent,
+ hide: !shouldDisplayLogTab,
+ hasBetaPill: true,
+ },
];
return (
@@ -131,6 +140,7 @@ export const ShowPageRightContainer = ({
)}
{activeTabId === 'emails' && }
{activeTabId === 'calendar' && }
+ {activeTabId === 'logs' && }
);
};
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/engine/api/graphql/workspace-query-runner/jobs/save-event-to-db.job.ts
index 82cab4e27..3b0d5a6ba 100644
--- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/save-event-to-db.job.ts
+++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/save-event-to-db.job.ts
@@ -2,12 +2,16 @@ import { Injectable } from '@nestjs/common';
import { MessageQueueJob } from 'src/engine/integrations/message-queue/interfaces/message-queue-job.interface';
-import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
-import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
+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 { 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;
@@ -16,39 +20,47 @@ export type SaveEventToDbJobData = {
@Injectable()
export class SaveEventToDbJob implements MessageQueueJob {
constructor(
- private readonly dataSourceService: DataSourceService,
- private readonly workspaceDataSourceService: WorkspaceDataSourceService,
+ @InjectObjectMetadataRepository(WorkspaceMemberObjectMetadata)
+ private readonly workspaceMemberService: WorkspaceMemberRepository,
+ @InjectObjectMetadataRepository(EventObjectMetadata)
+ private readonly eventService: EventRepository,
) {}
+ // TODO: need to support objects others than "person", "company", "opportunity"
async handle(data: SaveEventToDbJobData): Promise {
- const dataSourceMetadata =
- await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
- data.workspaceId,
- );
- const workspaceDataSource =
- await this.workspaceDataSourceService.connectToWorkspaceDataSource(
+ let workspaceMemberId: string | null = null;
+
+ if (data.userId) {
+ const workspaceMember = await this.workspaceMemberService.getByIdOrFail(
+ data.userId,
data.workspaceId,
);
- const eventType = `${data.operation}.${data.objectName}`;
-
- // TODO: add "workspaceMember" (who performed the action, need to send it in the event)
- // TODO: need to support objects others than "person", "company", "opportunities"
+ workspaceMemberId = workspaceMember.id;
+ }
if (
data.objectName != 'person' &&
data.objectName != 'company' &&
- data.objectName != 'opportunities'
+ data.objectName != 'opportunity'
) {
return;
}
- await workspaceDataSource?.query(
- `INSERT INTO ${dataSourceMetadata.schema}."event"
- ("name", "properties", "${data.objectName}Id")
- VALUES ('${eventType}', '${JSON.stringify(data.details)}', '${
- data.recordId
- }') RETURNING *`,
+ if (data.details.diff) {
+ // we remove "before" and "after" property for a cleaner/slimmer event payload
+ data.details = {
+ diff: data.details.diff,
+ };
+ }
+
+ await this.eventService.insert(
+ `${data.operation}.${data.objectName}`,
+ data.details,
+ workspaceMemberId,
+ data.objectName,
+ data.recordId,
+ data.workspaceId,
);
}
}
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 f13047daf..b77db9c38 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
@@ -15,6 +15,8 @@ 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';
@Injectable()
export class EntityEventsToDbListener {
@@ -31,7 +33,12 @@ export class EntityEventsToDbListener {
}
@OnEvent('*.updated')
- async handleUpdate(payload: ObjectRecordCreateEvent) {
+ async handleUpdate(payload: ObjectRecordUpdateEvent) {
+ payload.details.diff = objectRecordChangedValues(
+ payload.details.before,
+ payload.details.after,
+ );
+
return this.handle(payload, 'updated');
}
@@ -58,6 +65,7 @@ export class EntityEventsToDbListener {
this.messageQueueService.add(SaveEventToDbJob.name, {
workspaceId: payload.workspaceId,
+ userId: payload.userId,
recordId: payload.recordId,
objectName: payload.objectMetadata.nameSingular,
operation: operation,
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 f2dcdd203..9b4efe05b 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,6 +9,9 @@ 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';
import { WorkspaceQueryRunnerService } from './workspace-query-runner.service';
@@ -21,6 +24,10 @@ import { EntityEventsToDbListener } from './listeners/entity-events-to-db.listen
WorkspaceDataSourceModule,
WorkspacePreQueryHookModule,
TypeOrmModule.forFeature([Workspace, FeatureFlagEntity], 'core'),
+ ObjectMetadataRepositoryModule.forFeature([
+ WorkspaceMemberObjectMetadata,
+ EventObjectMetadata,
+ ]),
],
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 dd9ae0f08..c3b13e393 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
@@ -216,7 +216,7 @@ export class WorkspaceQueryRunnerService {
args: CreateManyResolverArgs,
options: WorkspaceQueryRunnerOptions,
): Promise {
- const { workspaceId, objectMetadataItem } = options;
+ const { workspaceId, userId, objectMetadataItem } = options;
const computedArgs = await this.queryRunnerArgsFactory.create(
args,
options,
@@ -246,6 +246,7 @@ export class WorkspaceQueryRunnerService {
parsedResults.forEach((record) => {
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.created`, {
workspaceId,
+ userId,
recordId: record.id,
objectMetadata: objectMetadataItem,
details: {
@@ -270,7 +271,7 @@ export class WorkspaceQueryRunnerService {
args: UpdateOneResolverArgs,
options: WorkspaceQueryRunnerOptions,
): Promise {
- const { workspaceId, objectMetadataItem } = options;
+ const { workspaceId, userId, objectMetadataItem } = options;
const existingRecord = await this.findOne(
{ filter: { id: { eq: args.id } } } as FindOneResolverArgs,
@@ -300,6 +301,7 @@ export class WorkspaceQueryRunnerService {
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.updated`, {
workspaceId,
+ userId,
recordId: (existingRecord as Record).id,
objectMetadata: objectMetadataItem,
details: {
@@ -356,7 +358,7 @@ export class WorkspaceQueryRunnerService {
args: DeleteManyResolverArgs,
options: WorkspaceQueryRunnerOptions,
): Promise {
- const { workspaceId, objectMetadataItem } = options;
+ const { workspaceId, userId, objectMetadataItem } = options;
const maximumRecordAffected = this.environmentService.get(
'MUTATION_MAXIMUM_RECORD_AFFECTED',
);
@@ -384,6 +386,7 @@ export class WorkspaceQueryRunnerService {
parsedResults.forEach((record) => {
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.deleted`, {
workspaceId,
+ userId,
recordId: record.id,
objectMetadata: objectMetadataItem,
details: {
@@ -399,7 +402,7 @@ export class WorkspaceQueryRunnerService {
args: DeleteOneResolverArgs,
options: WorkspaceQueryRunnerOptions,
): Promise {
- const { workspaceId, objectMetadataItem } = options;
+ const { workspaceId, userId, objectMetadataItem } = options;
const query = await this.workspaceQueryBuilderFactory.deleteOne(
args,
options,
@@ -422,6 +425,7 @@ export class WorkspaceQueryRunnerService {
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.deleted`, {
workspaceId,
+ userId,
recordId: args.id,
objectMetadata: objectMetadataItem,
details: {
diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/index.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/index.ts
index 42ddc8909..b362a0a11 100644
--- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/index.ts
+++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/index.ts
@@ -8,4 +8,4 @@ export * from './string-filter.input-type';
export * from './time-filter.input-type';
export * from './uuid-filter.input-type';
export * from './boolean-filter.input-type';
-export * from './json-filter.input-type';
+export * from './raw-json-filter.input-type';
diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/json-filter.input-type.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/raw-json-filter.input-type.ts
similarity index 71%
rename from packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/json-filter.input-type.ts
rename to packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/raw-json-filter.input-type.ts
index 6161bd86e..5b06437dd 100644
--- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/json-filter.input-type.ts
+++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/raw-json-filter.input-type.ts
@@ -2,8 +2,8 @@ import { GraphQLInputObjectType } from 'graphql';
import { FilterIs } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/filter-is.input-type';
-export const JsonFilterType = new GraphQLInputObjectType({
- name: 'JsonFilter',
+export const RawJsonFilterType = new GraphQLInputObjectType({
+ name: 'RawJsonFilter',
fields: {
is: { type: FilterIs },
},
diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts
index 6b072c2df..951e63042 100644
--- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts
+++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts
@@ -32,7 +32,7 @@ import {
IntFilterType,
BooleanFilterType,
BigFloatFilterType,
- JsonFilterType,
+ RawJsonFilterType,
} from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input';
import { OrderByDirectionType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/enum';
import { BigFloatScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@@ -70,7 +70,7 @@ export class TypeMapperService {
[FieldMetadataType.PROBABILITY, GraphQLFloat],
[FieldMetadataType.RELATION, GraphQLID],
[FieldMetadataType.POSITION, PositionScalarType],
- [FieldMetadataType.JSON, GraphQLJSON],
+ [FieldMetadataType.RAW_JSON, GraphQLJSON],
]);
return typeScalarMapping.get(fieldMetadataType);
@@ -102,7 +102,7 @@ export class TypeMapperService {
[FieldMetadataType.PROBABILITY, FloatFilterType],
[FieldMetadataType.RELATION, UUIDFilterType],
[FieldMetadataType.POSITION, FloatFilterType],
- [FieldMetadataType.JSON, JsonFilterType],
+ [FieldMetadataType.RAW_JSON, RawJsonFilterType],
]);
return typeFilterMapping.get(fieldMetadataType);
@@ -126,7 +126,7 @@ export class TypeMapperService {
[FieldMetadataType.SELECT, OrderByDirectionType],
[FieldMetadataType.MULTI_SELECT, OrderByDirectionType],
[FieldMetadataType.POSITION, OrderByDirectionType],
- [FieldMetadataType.JSON, OrderByDirectionType],
+ [FieldMetadataType.RAW_JSON, OrderByDirectionType],
]);
return typeOrderByMapping.get(fieldMetadataType);
diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts
index 487156e6b..50c72bbf8 100644
--- a/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts
+++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts
@@ -69,7 +69,7 @@ const getSchemaComponentsProperties = (
),
};
break;
- case FieldMetadataType.JSON:
+ case FieldMetadataType.RAW_JSON:
type: 'object';
break;
default:
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 ef6a6d387..7f6dbfd04 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
@@ -4,5 +4,6 @@ export class ObjectRecordUpdateEvent extends ObjectRecordBaseEvent {
details: {
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 24b593427..a82ed3e0a 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
@@ -3,6 +3,7 @@ import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metad
export class ObjectRecordBaseEvent {
workspaceId: string;
recordId: string;
+ userId?: string;
objectMetadata: ObjectMetadataInterface;
details: 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
new file mode 100644
index 000000000..abe8dad89
--- /dev/null
+++ b/packages/twenty-server/src/engine/integrations/event-emitter/utils/__tests__/object-record-changed-values.spec.ts
@@ -0,0 +1,65 @@
+import { objectRecordChangedValues } from 'src/engine/integrations/event-emitter/utils/object-record-changed-values';
+
+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);
+
+ expect(result).toEqual({
+ name: { before: 'Original Name', after: 'Updated Name' },
+ });
+ });
+});
+
+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);
+
+ expect(result).toEqual({});
+});
+
+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);
+
+ expect(result).toEqual({});
+});
+
+it('correctly handles a mix of changed, unchanged, and special case values', () => {
+ const oldRecord = {
+ id: 1,
+ name: 'Original',
+ status: 'active',
+ updatedAt: new Date(2020, 1, 1),
+ config: { theme: 'dark' },
+ };
+ const newRecord = {
+ id: 1,
+ name: 'Updated',
+ status: 'active',
+ updatedAt: new Date(2021, 1, 1),
+ config: { theme: 'light' },
+ };
+ const expectedChanges = {
+ name: { before: 'Original', after: 'Updated' },
+ };
+
+ const result = objectRecordChangedValues(oldRecord, newRecord);
+
+ 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
new file mode 100644
index 000000000..99d82d2bf
--- /dev/null
+++ b/packages/twenty-server/src/engine/integrations/event-emitter/utils/object-record-changed-values.ts
@@ -0,0 +1,28 @@
+import deepEqual from 'deep-equal';
+
+export const objectRecordChangedValues = (
+ oldRecord: Record,
+ newRecord: Record,
+) => {
+ 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])) {
+ return acc;
+ }
+
+ if (!deepEqual(oldRecord[key], newRecord[key]) && key != 'updatedAt') {
+ acc[key] = { before: oldRecord[key], after: newRecord[key] };
+ }
+
+ return acc;
+ },
+ {} as Record,
+ );
+
+ return changedValues;
+};
diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/default-value.input.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/default-value.input.ts
index 89830525a..1d2b5f86f 100644
--- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/default-value.input.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/default-value.input.ts
@@ -17,7 +17,7 @@ export class FieldMetadataDefaultValueString {
value: string | null;
}
-export class FieldMetadataDefaultValueJson {
+export class FieldMetadataDefaultValueRawJson {
@ValidateIf((_object, value) => value !== null)
@IsJSON()
value: JSON | null;
diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts
index 11bf1de22..e98831cea 100644
--- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts
@@ -36,7 +36,7 @@ export enum FieldMetadataType {
MULTI_SELECT = 'MULTI_SELECT',
RELATION = 'RELATION',
POSITION = 'POSITION',
- JSON = 'JSON',
+ RAW_JSON = 'RAW_JSON',
}
@Entity('fieldMetadata')
diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface.ts
index 191f077bc..e66e1249b 100644
--- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface.ts
@@ -3,7 +3,7 @@ import {
FieldMetadataDefaultValueCurrency,
FieldMetadataDefaultValueDateTime,
FieldMetadataDefaultValueFullName,
- FieldMetadataDefaultValueJson,
+ FieldMetadataDefaultValueRawJson,
FieldMetadataDefaultValueLink,
FieldMetadataDefaultValueNumber,
FieldMetadataDefaultValueString,
@@ -51,7 +51,7 @@ type FieldMetadataDefaultValueMapping = {
[FieldMetadataType.RATING]: FieldMetadataDefaultValueString;
[FieldMetadataType.SELECT]: FieldMetadataDefaultValueString;
[FieldMetadataType.MULTI_SELECT]: FieldMetadataDefaultValueStringArray;
- [FieldMetadataType.JSON]: FieldMetadataDefaultValueJson;
+ [FieldMetadataType.RAW_JSON]: FieldMetadataDefaultValueRawJson;
};
type DefaultValueByFieldMetadata = [
diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-target-column-map.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-target-column-map.util.ts
index e1d469439..e9d3799d6 100644
--- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-target-column-map.util.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-target-column-map.util.ts
@@ -35,7 +35,7 @@ export function generateTargetColumnMap(
case FieldMetadataType.SELECT:
case FieldMetadataType.MULTI_SELECT:
case FieldMetadataType.POSITION:
- case FieldMetadataType.JSON:
+ case FieldMetadataType.RAW_JSON:
return {
value: columnName,
};
diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util.ts
index 4c436bff8..5da968a42 100644
--- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util.ts
@@ -9,7 +9,7 @@ import {
FieldMetadataDefaultValueCurrency,
FieldMetadataDefaultValueDateTime,
FieldMetadataDefaultValueFullName,
- FieldMetadataDefaultValueJson,
+ FieldMetadataDefaultValueRawJson,
FieldMetadataDefaultValueLink,
FieldMetadataDefaultValueNumber,
FieldMetadataDefaultValueString,
@@ -40,7 +40,7 @@ export const defaultValueValidatorsMap = {
[FieldMetadataType.RATING]: [FieldMetadataDefaultValueString],
[FieldMetadataType.SELECT]: [FieldMetadataDefaultValueString],
[FieldMetadataType.MULTI_SELECT]: [FieldMetadataDefaultValueStringArray],
- [FieldMetadataType.JSON]: [FieldMetadataDefaultValueJson],
+ [FieldMetadataType.RAW_JSON]: [FieldMetadataDefaultValueRawJson],
};
export const validateDefaultValueForType = (
diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util.ts
index cb309c3a4..c413da18c 100644
--- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util.ts
@@ -29,7 +29,7 @@ export const fieldMetadataTypeToColumnType = (
case FieldMetadataType.SELECT:
case FieldMetadataType.MULTI_SELECT:
return 'enum';
- case FieldMetadataType.JSON:
+ case FieldMetadataType.RAW_JSON:
return 'jsonb';
default:
throw new Error(`Cannot convert ${fieldMetadataType} to column type.`);
diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.factory.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.factory.ts
index 8a4faf5a6..bd884e821 100644
--- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.factory.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.factory.ts
@@ -67,7 +67,7 @@ export class WorkspaceMigrationFactory {
[FieldMetadataType.NUMERIC, { factory: this.basicColumnActionFactory }],
[FieldMetadataType.NUMBER, { factory: this.basicColumnActionFactory }],
[FieldMetadataType.POSITION, { factory: this.basicColumnActionFactory }],
- [FieldMetadataType.JSON, { factory: this.basicColumnActionFactory }],
+ [FieldMetadataType.RAW_JSON, { factory: this.basicColumnActionFactory }],
[
FieldMetadataType.PROBABILITY,
{ factory: this.basicColumnActionFactory },
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 593bde6e5..427b432f2 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,6 +5,7 @@ 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 { 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';
@@ -22,6 +23,7 @@ export const metadataToRepositoryMapping = {
CalendarEventObjectMetadata: CalendarEventRepository,
CompanyObjectMetadata: CompanyRepository,
ConnectedAccountObjectMetadata: ConnectedAccountRepository,
+ EventObjectMetadata: EventRepository,
MessageChannelMessageAssociationObjectMetadata:
MessageChannelMessageAssociationRepository,
MessageChannelObjectMetadata: MessageChannelRepository,
diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/utils/map-field-metadata-type-to-data-type.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/utils/map-field-metadata-type-to-data-type.util.ts
index c8999c718..f5fa6a893 100644
--- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/utils/map-field-metadata-type-to-data-type.util.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/workspace-health/utils/map-field-metadata-type-to-data-type.util.ts
@@ -22,7 +22,7 @@ export const mapFieldMetadataTypeToDataType = (
return 'boolean';
case FieldMetadataType.DATE_TIME:
return 'timestamp';
- case FieldMetadataType.JSON:
+ case FieldMetadataType.RAW_JSON:
return 'jsonb';
case FieldMetadataType.RATING:
case FieldMetadataType.SELECT:
diff --git a/packages/twenty-server/src/modules/event/repositiories/event.repository.ts b/packages/twenty-server/src/modules/event/repositiories/event.repository.ts
new file mode 100644
index 000000000..1da98ee59
--- /dev/null
+++ b/packages/twenty-server/src/modules/event/repositiories/event.repository.ts
@@ -0,0 +1,30 @@
+import { Injectable } from '@nestjs/common';
+
+import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
+
+@Injectable()
+export class EventRepository {
+ constructor(
+ private readonly workspaceDataSourceService: WorkspaceDataSourceService,
+ ) {}
+
+ public async insert(
+ name: string,
+ properties: string,
+ workspaceMemberId: string | null,
+ objectName: string,
+ objectId: 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],
+ workspaceId,
+ );
+ }
+}
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 78cc91bbf..ec29cdaa9 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
@@ -39,7 +39,7 @@ export class EventObjectMetadata extends BaseObjectMetadata {
@FieldMetadata({
standardId: eventStandardFieldIds.properties,
- type: FieldMetadataType.JSON,
+ type: FieldMetadataType.RAW_JSON,
label: 'Event details',
description: 'Json value for event details',
icon: 'IconListDetails',