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',