diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 5ae4a8999..63d820235 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -66,10 +66,32 @@ export type AuthTokens = { export type Billing = { __typename?: 'Billing'; billingFreeTrialDurationInDays?: Maybe; - billingUrl: Scalars['String']['output']; + billingUrl?: Maybe; isBillingEnabled: Scalars['Boolean']['output']; }; +export type BillingSubscription = { + __typename?: 'BillingSubscription'; + id: Scalars['ID']['output']; + status: Scalars['String']['output']; +}; + +export type BillingSubscriptionFilter = { + and?: InputMaybe>; + id?: InputMaybe; + or?: InputMaybe>; +}; + +export type BillingSubscriptionSort = { + direction: SortDirection; + field: BillingSubscriptionSortFields; + nulls?: InputMaybe; +}; + +export enum BillingSubscriptionSortFields { + Id = 'id' +} + export type BooleanFieldComparison = { is?: InputMaybe; isNot?: InputMaybe; @@ -241,6 +263,7 @@ export enum FieldMetadataType { DateTime = 'DATE_TIME', Email = 'EMAIL', FullName = 'FULL_NAME', + Json = 'JSON', Link = 'LINK', MultiSelect = 'MULTI_SELECT', Number = 'NUMBER', @@ -301,7 +324,6 @@ export type Mutation = { activateWorkspace: Workspace; challenge: LoginToken; checkoutSession: SessionEntity; - createEvent: Analytics; createOneField: Field; createOneObject: Object; createOneRefreshToken: RefreshToken; @@ -318,6 +340,7 @@ export type Mutation = { impersonate: Verify; renewToken: AuthTokens; signUp: LoginToken; + track: Analytics; updateOneField: Field; updateOneObject: Object; updatePasswordViaResetToken: InvalidatePassword; @@ -347,12 +370,6 @@ export type MutationCheckoutSessionArgs = { }; -export type MutationCreateEventArgs = { - data: Scalars['JSON']['input']; - type: Scalars['String']['input']; -}; - - export type MutationCreateOneFieldArgs = { input: CreateOneFieldMetadataInput; }; @@ -421,6 +438,12 @@ export type MutationSignUpArgs = { }; +export type MutationTrackArgs = { + data: Scalars['JSON']['input']; + type: Scalars['String']['input']; +}; + + export type MutationUpdateOneFieldArgs = { input: UpdateOneFieldMetadataInput; }; @@ -631,6 +654,23 @@ export type RelationConnection = { pageInfo: PageInfo; }; +export type RelationDefinition = { + __typename?: 'RelationDefinition'; + direction: RelationDefinitionType; + sourceFieldMetadata: Field; + sourceObjectMetadata: Object; + targetFieldMetadata: Field; + targetObjectMetadata: Object; +}; + +/** Relation definition type */ +export enum RelationDefinitionType { + ManyToMany = 'MANY_TO_MANY', + ManyToOne = 'MANY_TO_ONE', + OneToMany = 'ONE_TO_MANY', + OneToOne = 'ONE_TO_ONE' +} + export type RelationDeleteResponse = { __typename?: 'RelationDeleteResponse'; createdAt?: Maybe; @@ -831,7 +871,9 @@ export type Workspace = { __typename?: 'Workspace'; activationStatus: Scalars['String']['output']; allowImpersonation: Scalars['Boolean']['output']; + billingSubscriptions?: Maybe>; createdAt: Scalars['DateTime']['output']; + currentBillingSubscription?: Maybe; deletedAt?: Maybe; displayName?: Maybe; domainName?: Maybe; @@ -844,6 +886,12 @@ export type Workspace = { }; +export type WorkspaceBillingSubscriptionsArgs = { + filter?: BillingSubscriptionFilter; + sorting?: Array; +}; + + export type WorkspaceFeatureFlagsArgs = { filter?: FeatureFlagFilter; sorting?: Array; @@ -886,6 +934,7 @@ export type Field = { label: Scalars['String']['output']; name: Scalars['String']['output']; options?: Maybe; + relationDefinition?: Maybe; toRelationMetadata?: Maybe; type: FieldMetadataType; updatedAt: Scalars['DateTime']['output']; diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 973de1bd5..abf8cb43f 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -183,6 +183,7 @@ export enum FieldMetadataType { DateTime = 'DATE_TIME', Email = 'EMAIL', FullName = 'FULL_NAME', + Json = 'JSON', Link = 'LINK', MultiSelect = 'MULTI_SELECT', Number = 'NUMBER', @@ -243,7 +244,6 @@ export type Mutation = { activateWorkspace: Workspace; challenge: LoginToken; checkoutSession: SessionEntity; - createEvent: Analytics; createOneObject: Object; createOneRefreshToken: RefreshToken; deleteCurrentWorkspace: Workspace; @@ -256,6 +256,7 @@ export type Mutation = { impersonate: Verify; renewToken: AuthTokens; signUp: LoginToken; + track: Analytics; updateOneObject: Object; updatePasswordViaResetToken: InvalidatePassword; updateWorkspace: Workspace; @@ -284,12 +285,6 @@ export type MutationCheckoutSessionArgs = { }; -export type MutationCreateEventArgs = { - data: Scalars['JSON']; - type: Scalars['String']; -}; - - export type MutationDeleteOneObjectArgs = { input: DeleteOneObjectInput; }; @@ -328,6 +323,12 @@ export type MutationSignUpArgs = { }; +export type MutationTrackArgs = { + data: Scalars['JSON']; + type: Scalars['String']; +}; + + export type MutationUpdatePasswordViaResetTokenArgs = { newPassword: Scalars['String']; passwordResetToken: Scalars['String']; @@ -917,13 +918,13 @@ export type GetTimelineThreadsFromPersonIdQuery = { __typename?: 'Query', getTim export type TimelineThreadFragment = { __typename?: 'TimelineThread', id: string, subject: string, lastMessageReceivedAt: string }; -export type CreateEventMutationVariables = Exact<{ +export type TrackMutationVariables = Exact<{ type: Scalars['String']; data: Scalars['JSON']; }>; -export type CreateEventMutation = { __typename?: 'Mutation', createEvent: { __typename?: 'Analytics', success: boolean } }; +export type TrackMutation = { __typename?: 'Mutation', track: { __typename?: 'Analytics', success: boolean } }; export type AuthTokenFragmentFragment = { __typename?: 'AuthToken', token: string, expiresAt: string }; @@ -1397,40 +1398,40 @@ export function useGetTimelineThreadsFromPersonIdLazyQuery(baseOptions?: Apollo. export type GetTimelineThreadsFromPersonIdQueryHookResult = ReturnType; export type GetTimelineThreadsFromPersonIdLazyQueryHookResult = ReturnType; export type GetTimelineThreadsFromPersonIdQueryResult = Apollo.QueryResult; -export const CreateEventDocument = gql` - mutation CreateEvent($type: String!, $data: JSON!) { - createEvent(type: $type, data: $data) { +export const TrackDocument = gql` + mutation Track($type: String!, $data: JSON!) { + track(type: $type, data: $data) { success } } `; -export type CreateEventMutationFn = Apollo.MutationFunction; +export type TrackMutationFn = Apollo.MutationFunction; /** - * __useCreateEventMutation__ + * __useTrackMutation__ * - * To run a mutation, you first call `useCreateEventMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useCreateEventMutation` returns a tuple that includes: + * To run a mutation, you first call `useTrackMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useTrackMutation` 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 [createEventMutation, { data, loading, error }] = useCreateEventMutation({ + * const [trackMutation, { data, loading, error }] = useTrackMutation({ * variables: { * type: // value for 'type' * data: // value for 'data' * }, * }); */ -export function useCreateEventMutation(baseOptions?: Apollo.MutationHookOptions) { +export function useTrackMutation(baseOptions?: Apollo.MutationHookOptions) { const options = {...defaultOptions, ...baseOptions} - return Apollo.useMutation(CreateEventDocument, options); + return Apollo.useMutation(TrackDocument, options); } -export type CreateEventMutationHookResult = ReturnType; -export type CreateEventMutationResult = Apollo.MutationResult; -export type CreateEventMutationOptions = Apollo.BaseMutationOptions; +export type TrackMutationHookResult = ReturnType; +export type TrackMutationResult = Apollo.MutationResult; +export type TrackMutationOptions = Apollo.BaseMutationOptions; export const ChallengeDocument = gql` mutation Challenge($email: String!, $password: String!) { challenge(email: $email, password: $password) { diff --git a/packages/twenty-front/src/modules/analytics/graphql/queries/createEvent.ts b/packages/twenty-front/src/modules/analytics/graphql/queries/createEvent.ts deleted file mode 100644 index 6183b9107..000000000 --- a/packages/twenty-front/src/modules/analytics/graphql/queries/createEvent.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { gql } from '@apollo/client'; - -export const CREATE_EVENT = gql` - mutation CreateEvent($type: String!, $data: JSON!) { - createEvent(type: $type, data: $data) { - success - } - } -`; diff --git a/packages/twenty-front/src/modules/analytics/graphql/queries/track.ts b/packages/twenty-front/src/modules/analytics/graphql/queries/track.ts new file mode 100644 index 000000000..3aa7bba0f --- /dev/null +++ b/packages/twenty-front/src/modules/analytics/graphql/queries/track.ts @@ -0,0 +1,9 @@ +import { gql } from '@apollo/client'; + +export const TRACK = gql` + mutation Track($type: String!, $data: JSON!) { + track(type: $type, data: $data) { + success + } + } +`; diff --git a/packages/twenty-front/src/modules/analytics/hooks/__tests__/useEventTracker.test.tsx b/packages/twenty-front/src/modules/analytics/hooks/__tests__/useEventTracker.test.tsx index 85d60977d..9b60f1ffa 100644 --- a/packages/twenty-front/src/modules/analytics/hooks/__tests__/useEventTracker.test.tsx +++ b/packages/twenty-front/src/modules/analytics/hooks/__tests__/useEventTracker.test.tsx @@ -11,8 +11,8 @@ const mocks: MockedResponse[] = [ { request: { query: gql` - mutation CreateEvent($type: String!, $data: JSON!) { - createEvent(type: $type, data: $data) { + mutation Track($type: String!, $data: JSON!) { + track(type: $type, data: $data) { success } } @@ -24,7 +24,7 @@ const mocks: MockedResponse[] = [ }, result: jest.fn(() => ({ data: { - createEvent: { + track: { success: true, }, }, diff --git a/packages/twenty-front/src/modules/analytics/hooks/__tests__/useTrackEvent.test.tsx b/packages/twenty-front/src/modules/analytics/hooks/__tests__/useTrackEvent.test.tsx index 69eeecdf1..f82b7323a 100644 --- a/packages/twenty-front/src/modules/analytics/hooks/__tests__/useTrackEvent.test.tsx +++ b/packages/twenty-front/src/modules/analytics/hooks/__tests__/useTrackEvent.test.tsx @@ -4,10 +4,10 @@ import { RecoilRoot } from 'recoil'; import { useTrackEvent } from '../useTrackEvent'; -const mockCreateEventMutation = jest.fn(); +const mockTrackMutation = jest.fn(); jest.mock('~/generated/graphql', () => ({ - useCreateEventMutation: () => [mockCreateEventMutation], + useTrackMutation: () => [mockTrackMutation], })); describe('useTrackEvent', () => { @@ -17,8 +17,8 @@ describe('useTrackEvent', () => { renderHook(() => useTrackEvent(eventType, eventData), { wrapper: RecoilRoot, }); - expect(mockCreateEventMutation).toHaveBeenCalledTimes(1); - expect(mockCreateEventMutation).toHaveBeenCalledWith({ + expect(mockTrackMutation).toHaveBeenCalledTimes(1); + expect(mockTrackMutation).toHaveBeenCalledWith({ variables: { type: eventType, data: eventData }, }); }); diff --git a/packages/twenty-front/src/modules/analytics/hooks/useEventTracker.ts b/packages/twenty-front/src/modules/analytics/hooks/useEventTracker.ts index 75f80b4ba..d2c34d023 100644 --- a/packages/twenty-front/src/modules/analytics/hooks/useEventTracker.ts +++ b/packages/twenty-front/src/modules/analytics/hooks/useEventTracker.ts @@ -2,7 +2,7 @@ import { useCallback } from 'react'; import { useRecoilValue } from 'recoil'; import { telemetryState } from '@/client-config/states/telemetryState'; -import { useCreateEventMutation } from '~/generated/graphql'; +import { useTrackMutation } from '~/generated/graphql'; interface EventLocation { pathname: string; @@ -14,7 +14,7 @@ export interface EventData { export const useEventTracker = () => { const telemetry = useRecoilValue(telemetryState()); - const [createEventMutation] = useCreateEventMutation(); + const [createEventMutation] = useTrackMutation(); return useCallback( (eventType: string, eventData: EventData) => { diff --git a/packages/twenty-front/src/modules/apollo/hooks/__tests__/useApolloFactory.test.tsx b/packages/twenty-front/src/modules/apollo/hooks/__tests__/useApolloFactory.test.tsx index bea66cc15..d1608ab93 100644 --- a/packages/twenty-front/src/modules/apollo/hooks/__tests__/useApolloFactory.test.tsx +++ b/packages/twenty-front/src/modules/apollo/hooks/__tests__/useApolloFactory.test.tsx @@ -77,8 +77,8 @@ describe('useApolloFactory', () => { await act(async () => { await result.current.factory.mutate({ mutation: gql` - mutation CreateEvent($type: String!, $data: JSON!) { - createEvent(type: $type, data: $data) { + mutation Track($type: String!, $data: JSON!) { + track(type: $type, data: $data) { success } } diff --git a/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts b/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts index 7bd43be26..d0ba37512 100644 --- a/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts +++ b/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts @@ -41,8 +41,8 @@ const makeRequest = async () => { await client.mutate({ mutation: gql` - mutation CreateEvent($type: String!, $data: JSON!) { - createEvent(type: $type, data: $data) { + mutation Track($type: String!, $data: JSON!) { + track(type: $type, data: $data) { success } } diff --git a/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts b/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts index 6b679b7d9..8c3fa5df9 100644 --- a/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts +++ b/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts @@ -1,4 +1,5 @@ export type FeatureFlagKey = | 'IS_BLOCKLIST_ENABLED' | 'IS_CALENDAR_ENABLED' - | 'IS_QUICK_ACTIONS_ENABLED'; + | 'IS_QUICK_ACTIONS_ENABLED' + | 'IS_EVENT_OBJECT_ENABLED'; diff --git a/packages/twenty-front/src/testing/graphqlMocks.ts b/packages/twenty-front/src/testing/graphqlMocks.ts index 9908092db..7bca3dbc3 100644 --- a/packages/twenty-front/src/testing/graphqlMocks.ts +++ b/packages/twenty-front/src/testing/graphqlMocks.ts @@ -1,7 +1,7 @@ import { getOperationName } from '@apollo/client/utilities'; import { graphql, HttpResponse } from 'msw'; -import { CREATE_EVENT } from '@/analytics/graphql/queries/createEvent'; +import { TRACK } from '@/analytics/graphql/queries/track'; import { GET_CLIENT_CONFIG } from '@/client-config/graphql/queries/getClientConfig'; import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries'; import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; @@ -31,10 +31,10 @@ export const graphqlMocks = { }, }); }), - graphql.mutation(getOperationName(CREATE_EVENT) ?? '', () => { + graphql.mutation(getOperationName(TRACK) ?? '', () => { return HttpResponse.json({ data: { - createEvent: { success: 1, __typename: 'Event' }, + track: { success: 1, __typename: 'TRACK' }, }, }); }), diff --git a/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts b/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts index 7391c5a81..98a4ce34b 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts @@ -25,6 +25,11 @@ export const seedFeatureFlags = async ( workspaceId: workspaceId, value: true, }, + { + key: FeatureFlagKeys.IsEventObjectEnabled, + workspaceId: workspaceId, + value: true, + }, ]) .execute(); }; diff --git a/packages/twenty-server/src/engine-metadata/field-metadata/dtos/default-value.input.ts b/packages/twenty-server/src/engine-metadata/field-metadata/dtos/default-value.input.ts index 812ff6591..89830525a 100644 --- a/packages/twenty-server/src/engine-metadata/field-metadata/dtos/default-value.input.ts +++ b/packages/twenty-server/src/engine-metadata/field-metadata/dtos/default-value.input.ts @@ -2,6 +2,7 @@ import { IsArray, IsBoolean, IsDate, + IsJSON, IsNotEmpty, IsNumber, IsNumberString, @@ -16,6 +17,12 @@ export class FieldMetadataDefaultValueString { value: string | null; } +export class FieldMetadataDefaultValueJson { + @ValidateIf((_object, value) => value !== null) + @IsJSON() + value: JSON | null; +} + export class FieldMetadataDefaultValueNumber { @ValidateIf((_object, value) => value !== null) @IsNumber() diff --git a/packages/twenty-server/src/engine-metadata/field-metadata/field-metadata.entity.ts b/packages/twenty-server/src/engine-metadata/field-metadata/field-metadata.entity.ts index f42446afa..2ac194c4d 100644 --- a/packages/twenty-server/src/engine-metadata/field-metadata/field-metadata.entity.ts +++ b/packages/twenty-server/src/engine-metadata/field-metadata/field-metadata.entity.ts @@ -36,6 +36,7 @@ export enum FieldMetadataType { MULTI_SELECT = 'MULTI_SELECT', RELATION = 'RELATION', POSITION = 'POSITION', + JSON = 'JSON', } @Entity('fieldMetadata') diff --git a/packages/twenty-server/src/engine-metadata/field-metadata/interfaces/field-metadata-default-value.interface.ts b/packages/twenty-server/src/engine-metadata/field-metadata/interfaces/field-metadata-default-value.interface.ts index cfe99ebf6..d08a8be1c 100644 --- a/packages/twenty-server/src/engine-metadata/field-metadata/interfaces/field-metadata-default-value.interface.ts +++ b/packages/twenty-server/src/engine-metadata/field-metadata/interfaces/field-metadata-default-value.interface.ts @@ -3,6 +3,7 @@ import { FieldMetadataDefaultValueCurrency, FieldMetadataDefaultValueDateTime, FieldMetadataDefaultValueFullName, + FieldMetadataDefaultValueJson, FieldMetadataDefaultValueLink, FieldMetadataDefaultValueNumber, FieldMetadataDefaultValueString, @@ -50,6 +51,7 @@ type FieldMetadataDefaultValueMapping = { [FieldMetadataType.RATING]: FieldMetadataDefaultValueString; [FieldMetadataType.SELECT]: FieldMetadataDefaultValueString; [FieldMetadataType.MULTI_SELECT]: FieldMetadataDefaultValueStringArray; + [FieldMetadataType.JSON]: FieldMetadataDefaultValueJson; }; type DefaultValueByFieldMetadata = [ diff --git a/packages/twenty-server/src/engine-metadata/field-metadata/utils/generate-target-column-map.util.ts b/packages/twenty-server/src/engine-metadata/field-metadata/utils/generate-target-column-map.util.ts index c44dffbfc..54161679c 100644 --- a/packages/twenty-server/src/engine-metadata/field-metadata/utils/generate-target-column-map.util.ts +++ b/packages/twenty-server/src/engine-metadata/field-metadata/utils/generate-target-column-map.util.ts @@ -35,6 +35,7 @@ export function generateTargetColumnMap( case FieldMetadataType.SELECT: case FieldMetadataType.MULTI_SELECT: case FieldMetadataType.POSITION: + case FieldMetadataType.JSON: return { value: columnName, }; diff --git a/packages/twenty-server/src/engine-metadata/field-metadata/utils/validate-default-value-for-type.util.ts b/packages/twenty-server/src/engine-metadata/field-metadata/utils/validate-default-value-for-type.util.ts index 3c936497d..9e9377e24 100644 --- a/packages/twenty-server/src/engine-metadata/field-metadata/utils/validate-default-value-for-type.util.ts +++ b/packages/twenty-server/src/engine-metadata/field-metadata/utils/validate-default-value-for-type.util.ts @@ -9,6 +9,7 @@ import { FieldMetadataDefaultValueCurrency, FieldMetadataDefaultValueDateTime, FieldMetadataDefaultValueFullName, + FieldMetadataDefaultValueJson, FieldMetadataDefaultValueLink, FieldMetadataDefaultValueNumber, FieldMetadataDefaultValueString, @@ -39,6 +40,7 @@ export const defaultValueValidatorsMap = { [FieldMetadataType.RATING]: [FieldMetadataDefaultValueString], [FieldMetadataType.SELECT]: [FieldMetadataDefaultValueString], [FieldMetadataType.MULTI_SELECT]: [FieldMetadataDefaultValueStringArray], + [FieldMetadataType.JSON]: [FieldMetadataDefaultValueJson], }; export const validateDefaultValueForType = ( diff --git a/packages/twenty-server/src/engine-metadata/workspace-migration/utils/field-metadata-type-to-column-type.util.ts b/packages/twenty-server/src/engine-metadata/workspace-migration/utils/field-metadata-type-to-column-type.util.ts index 848040f8b..36bf8c30a 100644 --- a/packages/twenty-server/src/engine-metadata/workspace-migration/utils/field-metadata-type-to-column-type.util.ts +++ b/packages/twenty-server/src/engine-metadata/workspace-migration/utils/field-metadata-type-to-column-type.util.ts @@ -29,6 +29,8 @@ export const fieldMetadataTypeToColumnType = ( case FieldMetadataType.SELECT: case FieldMetadataType.MULTI_SELECT: return 'enum'; + case FieldMetadataType.JSON: + return 'jsonb'; default: throw new Error(`Cannot convert ${fieldMetadataType} to column type.`); } diff --git a/packages/twenty-server/src/engine-metadata/workspace-migration/workspace-migration.factory.ts b/packages/twenty-server/src/engine-metadata/workspace-migration/workspace-migration.factory.ts index c3709df5f..55819d2d6 100644 --- a/packages/twenty-server/src/engine-metadata/workspace-migration/workspace-migration.factory.ts +++ b/packages/twenty-server/src/engine-metadata/workspace-migration/workspace-migration.factory.ts @@ -67,6 +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.PROBABILITY, { factory: this.basicColumnActionFactory }, 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 new file mode 100644 index 000000000..44fbad59c --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/save-event-to-db.job.ts @@ -0,0 +1,54 @@ +import { Injectable } from '@nestjs/common'; + +import { MessageQueueJob } from 'src/engine/integrations/message-queue/interfaces/message-queue-job.interface'; + +import { DataSourceService } from 'src/engine-metadata/data-source/data-source.service'; +import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; + +export type SaveEventToDbJobData = { + workspaceId: string; + recordId: string; + objectName: string; + operation: string; + details: any; +}; + +@Injectable() +export class SaveEventToDbJob implements MessageQueueJob { + constructor( + private readonly dataSourceService: DataSourceService, + private readonly workspaceDataSourceService: WorkspaceDataSourceService, + ) {} + + async handle(data: SaveEventToDbJobData): Promise { + const dataSourceMetadata = + await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail( + data.workspaceId, + ); + const workspaceDataSource = + await this.workspaceDataSourceService.connectToWorkspaceDataSource( + 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" + + if ( + data.objectName != 'person' && + data.objectName != 'company' && + data.objectName != 'opportunities' + ) { + return; + } + + await workspaceDataSource?.query( + `INSERT INTO ${dataSourceMetadata.schema}."event" + ("name", "properties", "${data.objectName}Id") + VALUES ('${eventType}', '${JSON.stringify(data.details)}', '${ + data.recordId + }') RETURNING *`, + ); + } +} 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 new file mode 100644 index 000000000..6f6b25f8d --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/entity-events-to-db.listener.ts @@ -0,0 +1,67 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; + +import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; +import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service'; +import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event'; +import { + SaveEventToDbJobData, + SaveEventToDbJob, +} from 'src/engine/api/graphql/workspace-query-runner/jobs/save-event-to-db.job'; +import { + FeatureFlagEntity, + FeatureFlagKeys, +} from 'src/engine/modules/feature-flag/feature-flag.entity'; + +@Injectable() +export class EntityEventsToDbListener { + constructor( + @Inject(MessageQueue.entityEventsToDbQueue) + private readonly messageQueueService: MessageQueueService, + @InjectRepository(FeatureFlagEntity, 'core') + private readonly featureFlagRepository: Repository, + ) {} + + @OnEvent('*.created') + async handleCreate(payload: ObjectRecordCreateEvent) { + return this.handle(payload, 'created'); + } + + @OnEvent('*.updated') + async handleUpdate(payload: ObjectRecordCreateEvent) { + return this.handle(payload, 'updated'); + } + + // @OnEvent('*.deleted') - TODO: implement when we have soft deleted + // .... + + private async handle( + payload: ObjectRecordCreateEvent, + operation: string, + ) { + const isEventObjectEnabledFeatureFlag = + await this.featureFlagRepository.findOneBy({ + workspaceId: payload.workspaceId, + key: FeatureFlagKeys.IsEventObjectEnabled, + value: true, + }); + + if ( + !isEventObjectEnabledFeatureFlag || + !isEventObjectEnabledFeatureFlag.value + ) { + return; + } + + this.messageQueueService.add(SaveEventToDbJob.name, { + workspaceId: payload.workspaceId, + recordId: payload.recordId, + objectName: payload.objectMetadata.nameSingular, + operation: operation, + details: payload.details, + }); + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/record-position.listener.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/record-position.listener.ts index c3fc08c61..8e3a23916 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/record-position.listener.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/record-position.listener.ts @@ -1,10 +1,9 @@ import { Inject, Injectable } from '@nestjs/common'; import { OnEvent } from '@nestjs/event-emitter'; -import { - CreatedObjectMetadata, - ObjectRecordCreateEvent, -} from 'src/engine/integrations/event-emitter/types/object-record-create.event'; +import { ObjectMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/object-metadata.interface'; + +import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event'; import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service'; import { @@ -21,11 +20,11 @@ export class RecordPositionListener { @OnEvent('*.created') async handleAllCreate(payload: ObjectRecordCreateEvent) { - if (!hasPositionField(payload.createdObjectMetadata)) { + if (!hasPositionField(payload.objectMetadata)) { return; } - if (hasPositionSet(payload.createdRecord)) { + if (hasPositionSet(payload.details.after)) { return; } @@ -33,15 +32,19 @@ export class RecordPositionListener { RecordPositionBackfillJob.name, { workspaceId: payload.workspaceId, - recordId: payload.createdRecord.id, - objectMetadata: payload.createdObjectMetadata, + recordId: payload.recordId, + objectMetadata: { + nameSingular: payload.objectMetadata.nameSingular, + isCustom: payload.objectMetadata.isCustom, + }, }, ); } } +// TODO: use objectMetadata instead of hardcoded standard objects name const hasPositionField = ( - createdObjectMetadata: CreatedObjectMetadata, + createdObjectMetadata: ObjectMetadataInterface, ): boolean => { return ( createdObjectMetadata.isCustom || 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 e96db3e36..b9feb83fa 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 @@ -1,4 +1,5 @@ import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; import { WorkspaceQueryBuilderModule } from 'src/engine/api/graphql/workspace-query-builder/workspace-query-builder.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; @@ -6,20 +7,26 @@ import { WorkspacePreQueryHookModule } from 'src/engine/api/graphql/workspace-qu import { workspaceQueryRunnerFactories } from 'src/engine/api/graphql/workspace-query-runner/factories'; import { RecordPositionListener } from 'src/engine/api/graphql/workspace-query-runner/listeners/record-position.listener'; import { AuthModule } from 'src/engine/modules/auth/auth.module'; +import { FeatureFlagEntity } from 'src/engine/modules/feature-flag/feature-flag.entity'; +import { Workspace } from 'src/engine/modules/workspace/workspace.entity'; import { WorkspaceQueryRunnerService } from './workspace-query-runner.service'; +import { EntityEventsToDbListener } from './listeners/entity-events-to-db.listener'; + @Module({ imports: [ AuthModule, WorkspaceQueryBuilderModule, WorkspaceDataSourceModule, WorkspacePreQueryHookModule, + TypeOrmModule.forFeature([Workspace, FeatureFlagEntity], 'core'), ], providers: [ WorkspaceQueryRunnerService, ...workspaceQueryRunnerFactories, RecordPositionListener, + EntityEventsToDbListener, ], exports: [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 620008a6a..1088f2ad7 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 @@ -246,10 +246,10 @@ export class WorkspaceQueryRunnerService { parsedResults.forEach((record) => { this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.created`, { workspaceId, - createdRecord: this.removeNestedProperties(record), - createdObjectMetadata: { - nameSingular: objectMetadataItem.nameSingular, - isCustom: objectMetadataItem.isCustom, + recordId: record.id, + objectMetadata: objectMetadataItem, + details: { + after: record, }, } satisfies ObjectRecordCreateEvent); }); @@ -300,8 +300,12 @@ export class WorkspaceQueryRunnerService { this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.updated`, { workspaceId, - previousRecord: this.removeNestedProperties(existingRecord as Record), - updatedRecord: this.removeNestedProperties(parsedResults?.[0]), + recordId: (existingRecord as Record).id, + objectMetadata: objectMetadataItem, + details: { + before: this.removeNestedProperties(existingRecord as Record), + after: this.removeNestedProperties(parsedResults?.[0]), + }, } satisfies ObjectRecordUpdateEvent); return parsedResults?.[0]; @@ -336,6 +340,12 @@ export class WorkspaceQueryRunnerService { options, ); + // TODO: check - NO EVENT SENT? + // OK I spent 2 hours trying to implement before/after diff and + // figured out why it hasn't been implement + // Doing a findMany in that context is very hard as long as we don't + // have a proper ORM. Let's come back to this once we do (target end of April 24?) + return parsedResults; } @@ -374,7 +384,11 @@ export class WorkspaceQueryRunnerService { parsedResults.forEach((record) => { this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.deleted`, { workspaceId, - deletedRecord: [this.removeNestedProperties(record)], + recordId: record.id, + objectMetadata: objectMetadataItem, + details: { + before: [this.removeNestedProperties(record)], + }, } satisfies ObjectRecordDeleteEvent); }); @@ -408,7 +422,11 @@ export class WorkspaceQueryRunnerService { this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.deleted`, { workspaceId, - deletedRecord: this.removeNestedProperties(parsedResults?.[0]), + recordId: args.id, + objectMetadata: objectMetadataItem, + details: { + before: this.removeNestedProperties(parsedResults?.[0]), + }, } satisfies ObjectRecordDeleteEvent); return parsedResults?.[0]; 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 31fea9680..42ddc8909 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,3 +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'; 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/json-filter.input-type.ts new file mode 100644 index 000000000..6161bd86e --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/json-filter.input-type.ts @@ -0,0 +1,10 @@ +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', + 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 d2df890aa..32a8c0b7c 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 @@ -15,6 +15,7 @@ import { GraphQLString, GraphQLType, } from 'graphql'; +import GraphQLJSON from 'graphql-type-json'; import { DateScalarMode, @@ -31,6 +32,7 @@ import { IntFilterType, BooleanFilterType, BigFloatFilterType, + JsonFilterType, } 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'; @@ -68,6 +70,7 @@ export class TypeMapperService { [FieldMetadataType.PROBABILITY, GraphQLFloat], [FieldMetadataType.RELATION, GraphQLID], [FieldMetadataType.POSITION, PositionScalarType], + [FieldMetadataType.JSON, GraphQLJSON], ]); return typeScalarMapping.get(fieldMetadataType); @@ -99,6 +102,7 @@ export class TypeMapperService { [FieldMetadataType.PROBABILITY, FloatFilterType], [FieldMetadataType.RELATION, UUIDFilterType], [FieldMetadataType.POSITION, FloatFilterType], + [FieldMetadataType.JSON, JsonFilterType], ]); return typeFilterMapping.get(fieldMetadataType); @@ -122,6 +126,7 @@ export class TypeMapperService { [FieldMetadataType.SELECT, OrderByDirectionType], [FieldMetadataType.MULTI_SELECT, OrderByDirectionType], [FieldMetadataType.POSITION, OrderByDirectionType], + [FieldMetadataType.JSON, OrderByDirectionType], ]); return typeOrderByMapping.get(fieldMetadataType); diff --git a/packages/twenty-server/src/engine/integrations/event-emitter/types/object-record-create.event.ts b/packages/twenty-server/src/engine/integrations/event-emitter/types/object-record-create.event.ts index fcfda8132..3e72f4fa3 100644 --- a/packages/twenty-server/src/engine/integrations/event-emitter/types/object-record-create.event.ts +++ b/packages/twenty-server/src/engine/integrations/event-emitter/types/object-record-create.event.ts @@ -1,12 +1,7 @@ -import { BaseObjectMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects/base.object-metadata'; +import { ObjectRecordBaseEvent } from 'src/engine/integrations/event-emitter/types/object-record.base.event'; -export type CreatedObjectMetadata = { - nameSingular: string; - isCustom: boolean; -}; - -export class ObjectRecordCreateEvent { - workspaceId: string; - createdRecord: T; - createdObjectMetadata: CreatedObjectMetadata; +export class ObjectRecordCreateEvent extends ObjectRecordBaseEvent { + details: { + after: T; + }; } diff --git a/packages/twenty-server/src/engine/integrations/event-emitter/types/object-record-delete.event.ts b/packages/twenty-server/src/engine/integrations/event-emitter/types/object-record-delete.event.ts index 6e7c011a9..b02f0a87b 100644 --- a/packages/twenty-server/src/engine/integrations/event-emitter/types/object-record-delete.event.ts +++ b/packages/twenty-server/src/engine/integrations/event-emitter/types/object-record-delete.event.ts @@ -1,6 +1,7 @@ -import { BaseObjectMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects/base.object-metadata'; +import { ObjectRecordBaseEvent } from 'src/engine/integrations/event-emitter/types/object-record.base.event'; -export declare class ObjectRecordDeleteEvent { - workspaceId: string; - deletedRecord: T; +export class ObjectRecordDeleteEvent extends ObjectRecordBaseEvent { + details: { + before: T; + }; } 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 6da59637d..ef6a6d387 100644 --- a/packages/twenty-server/src/engine/integrations/event-emitter/types/object-record-update.event.ts +++ b/packages/twenty-server/src/engine/integrations/event-emitter/types/object-record-update.event.ts @@ -1,7 +1,8 @@ -import { BaseObjectMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects/base.object-metadata'; +import { ObjectRecordBaseEvent } from 'src/engine/integrations/event-emitter/types/object-record.base.event'; -export class ObjectRecordUpdateEvent { - workspaceId: string; - previousRecord: T; - updatedRecord: T; +export class ObjectRecordUpdateEvent extends ObjectRecordBaseEvent { + details: { + before: T; + after: T; + }; } 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 new file mode 100644 index 000000000..d926f9efa --- /dev/null +++ b/packages/twenty-server/src/engine/integrations/event-emitter/types/object-record.base.event.ts @@ -0,0 +1,8 @@ +import { ObjectMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/object-metadata.interface'; + +export class ObjectRecordBaseEvent { + workspaceId: string; + recordId: string; + objectMetadata: ObjectMetadataInterface; + details: any; +} diff --git a/packages/twenty-server/src/engine/integrations/message-queue/jobs.module.ts b/packages/twenty-server/src/engine/integrations/message-queue/jobs.module.ts index 4bec99690..d2f056b72 100644 --- a/packages/twenty-server/src/engine/integrations/message-queue/jobs.module.ts +++ b/packages/twenty-server/src/engine/integrations/message-queue/jobs.module.ts @@ -44,6 +44,7 @@ import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repos import { ConnectedAccountObjectMetadata } from 'src/modules/connected-account/standard-objects/connected-account.object-metadata'; import { MessageParticipantObjectMetadata } from 'src/modules/messaging/standard-objects/message-participant.object-metadata'; import { MessageChannelObjectMetadata } from 'src/modules/messaging/standard-objects/message-channel.object-metadata'; +import { SaveEventToDbJob } from 'src/engine/api/graphql/workspace-query-runner/jobs/save-event-to-db.job'; @Module({ imports: [ @@ -130,6 +131,10 @@ import { MessageChannelObjectMetadata } from 'src/modules/messaging/standard-obj provide: RecordPositionBackfillJob.name, useClass: RecordPositionBackfillJob, }, + { + provide: SaveEventToDbJob.name, + useClass: SaveEventToDbJob, + }, ], }) export class JobsModule { diff --git a/packages/twenty-server/src/engine/integrations/message-queue/message-queue.constants.ts b/packages/twenty-server/src/engine/integrations/message-queue/message-queue.constants.ts index 7cb1fae15..e3a319e02 100644 --- a/packages/twenty-server/src/engine/integrations/message-queue/message-queue.constants.ts +++ b/packages/twenty-server/src/engine/integrations/message-queue/message-queue.constants.ts @@ -9,4 +9,5 @@ export enum MessageQueue { calendarQueue = 'calendar-queue', billingQueue = 'billing-queue', recordPositionBackfillQueue = 'record-position-backfill-queue', + entityEventsToDbQueue = 'entity-events-to-db-queue', } diff --git a/packages/twenty-server/src/engine/modules/analytics/analytics.resolver.ts b/packages/twenty-server/src/engine/modules/analytics/analytics.resolver.ts index 79e55563c..999b347dc 100644 --- a/packages/twenty-server/src/engine/modules/analytics/analytics.resolver.ts +++ b/packages/twenty-server/src/engine/modules/analytics/analytics.resolver.ts @@ -20,14 +20,14 @@ export class AnalyticsResolver { constructor(private readonly analyticsService: AnalyticsService) {} @Mutation(() => Analytics) - createEvent( - @Args() createEventInput: CreateAnalyticsInput, + track( + @Args() createAnalyticsInput: CreateAnalyticsInput, @AuthWorkspace() workspace: Workspace | undefined, @AuthUser({ allowUndefined: true }) user: User | undefined, @Context('req') request: Request, ) { return this.analyticsService.create( - createEventInput, + createAnalyticsInput, user, workspace, request, diff --git a/packages/twenty-server/src/engine/modules/feature-flag/feature-flag.entity.ts b/packages/twenty-server/src/engine/modules/feature-flag/feature-flag.entity.ts index 9ed988fb8..a3ae8c263 100644 --- a/packages/twenty-server/src/engine/modules/feature-flag/feature-flag.entity.ts +++ b/packages/twenty-server/src/engine/modules/feature-flag/feature-flag.entity.ts @@ -16,6 +16,7 @@ import { Workspace } from 'src/engine/modules/workspace/workspace.entity'; export enum FeatureFlagKeys { IsBlocklistEnabled = 'IS_BLOCKLIST_ENABLED', IsCalendarEnabled = 'IS_CALENDAR_ENABLED', + IsEventObjectEnabled = 'IS_EVENT_OBJECT_ENABLED', } @Entity({ name: 'featureFlag', schema: 'core' }) diff --git a/packages/twenty-server/src/engine/modules/open-api/utils/components.utils.ts b/packages/twenty-server/src/engine/modules/open-api/utils/components.utils.ts index b3336e880..7fc94e523 100644 --- a/packages/twenty-server/src/engine/modules/open-api/utils/components.utils.ts +++ b/packages/twenty-server/src/engine/modules/open-api/utils/components.utils.ts @@ -69,6 +69,9 @@ const getSchemaComponentsProperties = ( ), }; break; + case FieldMetadataType.JSON: + type: 'object'; + break; default: itemProperty.type = 'string'; break; diff --git a/packages/twenty-server/src/engine/modules/user-workspace/user-workspace.service.ts b/packages/twenty-server/src/engine/modules/user-workspace/user-workspace.service.ts index e6cacc139..b43afb61f 100644 --- a/packages/twenty-server/src/engine/modules/user-workspace/user-workspace.service.ts +++ b/packages/twenty-server/src/engine/modules/user-workspace/user-workspace.service.ts @@ -62,8 +62,10 @@ export class UserWorkspaceService extends TypeOrmQueryService { new ObjectRecordCreateEvent(); payload.workspaceId = workspaceId; - payload.createdRecord = new WorkspaceMemberObjectMetadata(); - payload.createdRecord = workspaceMember[0]; + payload.details = { + after: workspaceMember[0], + }; + payload.recordId = workspaceMember[0].id; this.eventEmitter.emit('workspaceMember.created', payload); } 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 cf35064ce..c219eeca6 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,6 +22,8 @@ export const mapFieldMetadataTypeToDataType = ( return 'boolean'; case FieldMetadataType.DATE_TIME: return 'timestamp'; + case FieldMetadataType.JSON: + return 'jsonb'; case FieldMetadataType.RATING: case FieldMetadataType.SELECT: case FieldMetadataType.MULTI_SELECT: diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/add-standard-id.command.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/add-standard-id.command.ts index 548a03257..8c70d3ecf 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/add-standard-id.command.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/add-standard-id.command.ts @@ -50,6 +50,7 @@ export class AddStandardIdCommand extends CommandRunner { { IS_BLOCKLIST_ENABLED: true, IS_CALENDAR_ENABLED: true, + IS_EVENT_OBJECT_ENABLED: true, }, ); const standardFieldMetadataCollection = this.standardFieldFactory.create( @@ -61,6 +62,7 @@ export class AddStandardIdCommand extends CommandRunner { { IS_BLOCKLIST_ENABLED: true, IS_CALENDAR_ENABLED: true, + IS_EVENT_OBJECT_ENABLED: true, }, ); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts index ee05ed3a2..f3cf6d23c 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts @@ -122,6 +122,7 @@ export const companyStandardFieldIds = { opportunities: '20202020-add3-4658-8e23-d70dccb6d0ec', favorites: '20202020-4d1d-41ac-b13b-621631298d55', attachments: '20202020-c1b5-4120-b0f0-987ca401ed53', + events: '20202020-0414-4daf-9c0d-64fe7b27f89f', }; export const connectedAccountStandardFieldIds = { @@ -135,6 +136,15 @@ export const connectedAccountStandardFieldIds = { calendarChannels: '20202020-af4a-47bb-99ec-51911c1d3977', }; +export const eventStandardFieldIds = { + properties: '20202020-f142-4b04-b91b-6a2b4af3bf10', + workspaceMember: '20202020-af23-4479-9a30-868edc474b35', + person: '20202020-c414-45b9-a60a-ac27aa96229e', + company: '20202020-04ad-4221-a744-7a8278a5ce20', + opportunity: '20202020-7664-4a35-a3df-580d389fd5f0', + custom: '20202020-4a71-41b0-9f83-9cdcca3f8b14', +}; + export const favoriteStandardFieldIds = { position: '20202020-dd26-42c6-8c3c-2a7598c204f6', workspaceMember: '20202020-ce63-49cb-9676-fdc0c45892cd', @@ -199,6 +209,7 @@ export const opportunityStandardFieldIds = { favorites: '20202020-a1c2-4500-aaae-83ba8a0e827a', activityTargets: '20202020-220a-42d6-8261-b2102d6eab35', attachments: '20202020-87c7-4118-83d6-2f4031005209', + events: '20202020-30e2-421f-96c7-19c69d1cf631', }; export const personStandardFieldIds = { @@ -218,6 +229,7 @@ export const personStandardFieldIds = { attachments: '20202020-cd97-451f-87fa-bcb789bdbf3a', messageParticipants: '20202020-498e-4c61-8158-fa04f0638334', calendarEventAttendees: '20202020-52ee-45e9-a702-b64b3753e3a9', + events: '20202020-a43e-4873-9c23-e522de906ce5', }; export const pipelineStepStandardFieldIds = { @@ -284,6 +296,7 @@ export const workspaceMemberStandardFieldIds = { messageParticipants: '20202020-8f99-48bc-a5eb-edd33dd54188', blocklist: '20202020-6cb2-4161-9f29-a4b7f1283859', calendarEventAttendees: '20202020-0dbc-4841-9ce1-3e793b5b3512', + events: '20202020-e15b-47b8-94fe-8200e3c66615', }; export const customObjectStandardFieldIds = { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids.ts index 40711e9b1..e155df556 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids.ts @@ -18,6 +18,7 @@ export const standardObjectIds = { comment: '20202020-435f-4de9-89b5-97e32233bf5f', company: '20202020-b374-4779-a561-80086cb2e17f', connectedAccount: '20202020-977e-46b2-890b-c3002ddfd5c5', + event: '20202020-6736-4337-b5c4-8b39fae325a5', favorite: '20202020-ab56-4e05-92a3-e2414a499860', messageChannelMessageAssociation: '20202020-ad1e-4127-bccb-d83ae04d2ccb', messageChannel: '20202020-fe8c-40bc-a681-b80b771449b7', diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-objects/index.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-objects/index.ts index 0c3cfe2e0..dd53ab77b 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-objects/index.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-objects/index.ts @@ -25,6 +25,7 @@ import { ViewObjectMetadata } from 'src/modules/view/standard-objects/view.objec import { WebhookObjectMetadata } from 'src/modules/webhook/standard-objects/webhook.object-metadata'; import { WorkspaceMemberObjectMetadata } from 'src/modules/workspace-member/standard-objects/workspace-member.object-metadata'; import { CalendarChannelEventAssociationObjectMetadata } from 'src/modules/calendar/standard-objects/calendar-channel-event-association.object-metadata'; +import { EventObjectMetadata } from 'src/modules/event/standard-objects/event.object-metadata'; export const standardObjectMetadataDefinitions = [ ActivityTargetObjectMetadata, @@ -35,6 +36,7 @@ export const standardObjectMetadataDefinitions = [ CommentObjectMetadata, CompanyObjectMetadata, ConnectedAccountObjectMetadata, + EventObjectMetadata, FavoriteObjectMetadata, OpportunityObjectMetadata, PersonObjectMetadata, diff --git a/packages/twenty-server/src/modules/company/standard-objects/company.object-metadata.ts b/packages/twenty-server/src/modules/company/standard-objects/company.object-metadata.ts index f951c066e..214e80439 100644 --- a/packages/twenty-server/src/modules/company/standard-objects/company.object-metadata.ts +++ b/packages/twenty-server/src/modules/company/standard-objects/company.object-metadata.ts @@ -19,6 +19,8 @@ import { FavoriteObjectMetadata } from 'src/modules/favorite/standard-objects/fa import { OpportunityObjectMetadata } from 'src/modules/opportunity/standard-objects/opportunity.object-metadata'; import { PersonObjectMetadata } from 'src/modules/person/standard-objects/person.object-metadata'; import { WorkspaceMemberObjectMetadata } from 'src/modules/workspace-member/standard-objects/workspace-member.object-metadata'; +import { EventObjectMetadata } from 'src/modules/event/standard-objects/event.object-metadata'; +import { Gate } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/gate.decorator'; @ObjectMetadata({ standardId: standardObjectIds.company, @@ -206,4 +208,23 @@ export class CompanyObjectMetadata extends BaseObjectMetadata { }) @IsNullable() attachments: AttachmentObjectMetadata[]; + + @FieldMetadata({ + standardId: companyStandardFieldIds.events, + type: FieldMetadataType.RELATION, + label: 'Events', + description: 'Events linked to the company', + icon: 'IconIconTimelineEvent', + }) + @RelationMetadata({ + type: RelationMetadataType.ONE_TO_MANY, + inverseSideTarget: () => EventObjectMetadata, + onDelete: RelationOnDeleteAction.CASCADE, + }) + @IsNullable() + @Gate({ + featureFlag: 'IS_EVENT_OBJECT_ENABLED', + }) + @IsSystem() + events: EventObjectMetadata[]; } 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 new file mode 100644 index 000000000..545370acf --- /dev/null +++ b/packages/twenty-server/src/modules/event/standard-objects/event.object-metadata.ts @@ -0,0 +1,103 @@ +import { FieldMetadataType } from 'src/engine-metadata/field-metadata/field-metadata.entity'; +import { FeatureFlagKeys } from 'src/engine/modules/feature-flag/feature-flag.entity'; +import { eventStandardFieldIds } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; +import { standardObjectIds } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; +import { CustomObjectMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/custom-objects/custom.object-metadata'; +import { DynamicRelationFieldMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/dynamic-field-metadata.interface'; +import { FieldMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/field-metadata.decorator'; +import { Gate } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/gate.decorator'; +import { IsNullable } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/is-nullable.decorator'; +import { IsSystem } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/is-system.decorator'; +import { ObjectMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/object-metadata.decorator'; +import { BaseObjectMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects/base.object-metadata'; +import { CompanyObjectMetadata } from 'src/modules/company/standard-objects/company.object-metadata'; +import { OpportunityObjectMetadata } from 'src/modules/opportunity/standard-objects/opportunity.object-metadata'; +import { PersonObjectMetadata } from 'src/modules/person/standard-objects/person.object-metadata'; +import { WorkspaceMemberObjectMetadata } from 'src/modules/workspace-member/standard-objects/workspace-member.object-metadata'; + +@ObjectMetadata({ + standardId: standardObjectIds.event, + namePlural: 'events', + labelSingular: 'Event', + labelPlural: 'Events', + description: 'An event', + icon: 'IconJson', +}) +@IsSystem() +@Gate({ + featureFlag: FeatureFlagKeys.IsEventObjectEnabled, +}) +export class EventObjectMetadata extends BaseObjectMetadata { + @FieldMetadata({ + standardId: eventStandardFieldIds.properties, + type: FieldMetadataType.TEXT, + label: 'Event name', + description: 'Event name/type', + icon: 'IconAbc', + }) + name: string; + + @FieldMetadata({ + standardId: eventStandardFieldIds.properties, + type: FieldMetadataType.JSON, + label: 'Event details', + description: 'Json value for event details', + icon: 'IconListDetails', + }) + @IsNullable() + properties: JSON; + + @FieldMetadata({ + standardId: eventStandardFieldIds.workspaceMember, + type: FieldMetadataType.RELATION, + label: 'Workspace Member', + description: 'Event workspace member', + icon: 'IconCircleUser', + joinColumn: 'workspaceMemberId', + }) + @IsNullable() + workspaceMember: WorkspaceMemberObjectMetadata; + + @FieldMetadata({ + standardId: eventStandardFieldIds.person, + type: FieldMetadataType.RELATION, + label: 'Person', + description: 'Event person', + icon: 'IconUser', + joinColumn: 'personId', + }) + @IsNullable() + person: PersonObjectMetadata; + + @FieldMetadata({ + standardId: eventStandardFieldIds.company, + type: FieldMetadataType.RELATION, + label: 'Company', + description: 'Event company', + icon: 'IconBuildingSkyscraper', + joinColumn: 'companyId', + }) + @IsNullable() + company: CompanyObjectMetadata; + + @FieldMetadata({ + standardId: eventStandardFieldIds.opportunity, + type: FieldMetadataType.RELATION, + label: 'Opportunity', + description: 'Events opportunity', + icon: 'IconTargetArrow', + joinColumn: 'opportunityId', + }) + @IsNullable() + opportunity: OpportunityObjectMetadata; + + @DynamicRelationFieldMetadata((oppositeObjectMetadata) => ({ + standardId: eventStandardFieldIds.custom, + name: oppositeObjectMetadata.nameSingular, + label: oppositeObjectMetadata.labelSingular, + description: `Favorite ${oppositeObjectMetadata.labelSingular}`, + joinColumn: `${oppositeObjectMetadata.nameSingular}Id`, + icon: 'IconBuildingSkyscraper', + })) + custom: CustomObjectMetadata; +} diff --git a/packages/twenty-server/src/modules/messaging/listeners/messaging-connected-account.listener.ts b/packages/twenty-server/src/modules/messaging/listeners/messaging-connected-account.listener.ts index f0f2ee0a0..d22bf5f36 100644 --- a/packages/twenty-server/src/modules/messaging/listeners/messaging-connected-account.listener.ts +++ b/packages/twenty-server/src/modules/messaging/listeners/messaging-connected-account.listener.ts @@ -46,7 +46,7 @@ export class MessagingConnectedAccountListener { DeleteConnectedAccountAssociatedMessagingDataJob.name, { workspaceId: payload.workspaceId, - connectedAccountId: payload.deletedRecord.id, + connectedAccountId: payload.recordId, }, ); @@ -55,7 +55,7 @@ export class MessagingConnectedAccountListener { DeleteConnectedAccountAssociatedCalendarDataJob.name, { workspaceId: payload.workspaceId, - connectedAccountId: payload.deletedRecord.id, + connectedAccountId: payload.recordId, }, ); } diff --git a/packages/twenty-server/src/modules/messaging/listeners/messaging-message-channel.listener.ts b/packages/twenty-server/src/modules/messaging/listeners/messaging-message-channel.listener.ts index 3176e245b..00a50a1c5 100644 --- a/packages/twenty-server/src/modules/messaging/listeners/messaging-message-channel.listener.ts +++ b/packages/twenty-server/src/modules/messaging/listeners/messaging-message-channel.listener.ts @@ -24,16 +24,16 @@ export class MessagingMessageChannelListener { ) { if ( objectRecordChangedProperties( - payload.previousRecord, - payload.updatedRecord, + payload.details.before, + payload.details.after, ).includes('isContactAutoCreationEnabled') && - payload.updatedRecord.isContactAutoCreationEnabled + payload.details.after.isContactAutoCreationEnabled ) { this.messageQueueService.add( CreateCompaniesAndContactsAfterSyncJob.name, { workspaceId: payload.workspaceId, - messageChannelId: payload.updatedRecord.id, + messageChannelId: payload.recordId, }, ); } diff --git a/packages/twenty-server/src/modules/messaging/listeners/messaging-person.listener.ts b/packages/twenty-server/src/modules/messaging/listeners/messaging-person.listener.ts index fa45120a1..df748ecaf 100644 --- a/packages/twenty-server/src/modules/messaging/listeners/messaging-person.listener.ts +++ b/packages/twenty-server/src/modules/messaging/listeners/messaging-person.listener.ts @@ -23,7 +23,7 @@ export class MessagingPersonListener { async handleCreatedEvent( payload: ObjectRecordCreateEvent, ) { - if (payload.createdRecord.email === null) { + if (payload.details.after.email === null) { return; } @@ -31,8 +31,8 @@ export class MessagingPersonListener { MatchMessageParticipantJob.name, { workspaceId: payload.workspaceId, - email: payload.createdRecord.email, - personId: payload.createdRecord.id, + email: payload.details.after.email, + personId: payload.recordId, }, ); } @@ -43,16 +43,16 @@ export class MessagingPersonListener { ) { if ( objectRecordUpdateEventChangedProperties( - payload.previousRecord, - payload.updatedRecord, + payload.details.before, + payload.details.after, ).includes('email') ) { this.messageQueueService.add( MatchMessageParticipantJob.name, { workspaceId: payload.workspaceId, - email: payload.updatedRecord.email, - personId: payload.updatedRecord.id, + email: payload.details.after.email, + personId: payload.recordId, }, ); } diff --git a/packages/twenty-server/src/modules/messaging/listeners/messaging-workspace-member.listener.ts b/packages/twenty-server/src/modules/messaging/listeners/messaging-workspace-member.listener.ts index d220f35ab..366bdab7c 100644 --- a/packages/twenty-server/src/modules/messaging/listeners/messaging-workspace-member.listener.ts +++ b/packages/twenty-server/src/modules/messaging/listeners/messaging-workspace-member.listener.ts @@ -23,7 +23,7 @@ export class MessagingWorkspaceMemberListener { async handleCreatedEvent( payload: ObjectRecordCreateEvent, ) { - if (payload.createdRecord.userEmail === null) { + if (payload.details.after.userEmail === null) { return; } @@ -31,8 +31,8 @@ export class MessagingWorkspaceMemberListener { MatchMessageParticipantJob.name, { workspaceId: payload.workspaceId, - email: payload.createdRecord.userEmail, - workspaceMemberId: payload.createdRecord.id, + email: payload.details.after.userEmail, + workspaceMemberId: payload.details.after.id, }, ); } @@ -43,16 +43,16 @@ export class MessagingWorkspaceMemberListener { ) { if ( objectRecordUpdateEventChangedProperties( - payload.previousRecord, - payload.updatedRecord, + payload.details.before, + payload.details.after, ).includes('userEmail') ) { await this.messageQueueService.add( MatchMessageParticipantJob.name, { workspaceId: payload.workspaceId, - email: payload.updatedRecord.userEmail, - workspaceMemberId: payload.updatedRecord.id, + email: payload.details.after.userEmail, + workspaceMemberId: payload.recordId, }, ); } diff --git a/packages/twenty-server/src/modules/opportunity/standard-objects/opportunity.object-metadata.ts b/packages/twenty-server/src/modules/opportunity/standard-objects/opportunity.object-metadata.ts index 13ac4f326..73af78b73 100644 --- a/packages/twenty-server/src/modules/opportunity/standard-objects/opportunity.object-metadata.ts +++ b/packages/twenty-server/src/modules/opportunity/standard-objects/opportunity.object-metadata.ts @@ -18,6 +18,8 @@ import { CompanyObjectMetadata } from 'src/modules/company/standard-objects/comp import { FavoriteObjectMetadata } from 'src/modules/favorite/standard-objects/favorite.object-metadata'; import { PersonObjectMetadata } from 'src/modules/person/standard-objects/person.object-metadata'; import { PipelineStepObjectMetadata } from 'src/modules/pipeline-step/standard-objects/pipeline-step.object-metadata'; +import { EventObjectMetadata } from 'src/modules/event/standard-objects/event.object-metadata'; +import { Gate } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/gate.decorator'; @ObjectMetadata({ standardId: standardObjectIds.opportunity, @@ -178,4 +180,21 @@ export class OpportunityObjectMetadata extends BaseObjectMetadata { }) @IsNullable() attachments: AttachmentObjectMetadata[]; + + @FieldMetadata({ + standardId: opportunityStandardFieldIds.events, + type: FieldMetadataType.RELATION, + label: 'Events', + description: 'Events linked to the opportunity.', + icon: 'IconTimelineEvent', + }) + @RelationMetadata({ + type: RelationMetadataType.ONE_TO_MANY, + inverseSideTarget: () => EventObjectMetadata, + }) + @Gate({ + featureFlag: 'IS_EVENT_OBJECT_ENABLED', + }) + @IsNullable() + events: EventObjectMetadata[]; } diff --git a/packages/twenty-server/src/modules/person/standard-objects/person.object-metadata.ts b/packages/twenty-server/src/modules/person/standard-objects/person.object-metadata.ts index 3e84b9f96..1461c49af 100644 --- a/packages/twenty-server/src/modules/person/standard-objects/person.object-metadata.ts +++ b/packages/twenty-server/src/modules/person/standard-objects/person.object-metadata.ts @@ -21,6 +21,7 @@ import { CompanyObjectMetadata } from 'src/modules/company/standard-objects/comp import { FavoriteObjectMetadata } from 'src/modules/favorite/standard-objects/favorite.object-metadata'; import { MessageParticipantObjectMetadata } from 'src/modules/messaging/standard-objects/message-participant.object-metadata'; import { OpportunityObjectMetadata } from 'src/modules/opportunity/standard-objects/opportunity.object-metadata'; +import { EventObjectMetadata } from 'src/modules/event/standard-objects/event.object-metadata'; @ObjectMetadata({ standardId: standardObjectIds.person, @@ -218,4 +219,23 @@ export class PersonObjectMetadata extends BaseObjectMetadata { }) @IsSystem() calendarEventAttendees: CalendarEventAttendeeObjectMetadata[]; + + @FieldMetadata({ + standardId: personStandardFieldIds.events, + type: FieldMetadataType.RELATION, + label: 'Events', + description: 'Events linked to the company', + icon: 'IconTimelineEvent', + }) + @RelationMetadata({ + type: RelationMetadataType.ONE_TO_MANY, + inverseSideTarget: () => EventObjectMetadata, + onDelete: RelationOnDeleteAction.CASCADE, + }) + @IsNullable() + @Gate({ + featureFlag: 'IS_EVENT_OBJECT_ENABLED', + }) + @IsSystem() + events: EventObjectMetadata[]; } diff --git a/packages/twenty-server/src/modules/workspace-member/standard-objects/workspace-member.object-metadata.ts b/packages/twenty-server/src/modules/workspace-member/standard-objects/workspace-member.object-metadata.ts index 4db6b2261..6c42fd515 100644 --- a/packages/twenty-server/src/modules/workspace-member/standard-objects/workspace-member.object-metadata.ts +++ b/packages/twenty-server/src/modules/workspace-member/standard-objects/workspace-member.object-metadata.ts @@ -21,6 +21,8 @@ import { CompanyObjectMetadata } from 'src/modules/company/standard-objects/comp import { ConnectedAccountObjectMetadata } from 'src/modules/connected-account/standard-objects/connected-account.object-metadata'; import { FavoriteObjectMetadata } from 'src/modules/favorite/standard-objects/favorite.object-metadata'; import { MessageParticipantObjectMetadata } from 'src/modules/messaging/standard-objects/message-participant.object-metadata'; +import { IsNullable } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/is-nullable.decorator'; +import { EventObjectMetadata } from 'src/modules/event/standard-objects/event.object-metadata'; @ObjectMetadata({ standardId: standardObjectIds.workspaceMember, @@ -231,4 +233,23 @@ export class WorkspaceMemberObjectMetadata extends BaseObjectMetadata { featureFlag: 'IS_CALENDAR_ENABLED', }) calendarEventAttendees: CalendarEventAttendeeObjectMetadata[]; + + @FieldMetadata({ + standardId: workspaceMemberStandardFieldIds.events, + type: FieldMetadataType.RELATION, + label: 'Events', + description: 'Events linked to the workspace member', + icon: 'IconTimelineEvent', + }) + @RelationMetadata({ + type: RelationMetadataType.ONE_TO_MANY, + inverseSideTarget: () => EventObjectMetadata, + onDelete: RelationOnDeleteAction.CASCADE, + }) + @IsNullable() + @Gate({ + featureFlag: 'IS_EVENT_OBJECT_ENABLED', + }) + @IsSystem() + events: EventObjectMetadata[]; }