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 <corentin@twenty.com>

* Move db calls to a dedicated repository

* Add server-side tests

---------

Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
Félix Malfait
2024-03-22 14:01:16 +01:00
committed by GitHub
parent aee6d49ea9
commit d876b40056
38 changed files with 488 additions and 95 deletions

View File

@@ -73,6 +73,7 @@ export type Billing = {
export type BillingSubscription = { export type BillingSubscription = {
__typename?: 'BillingSubscription'; __typename?: 'BillingSubscription';
id: Scalars['ID']['output']; id: Scalars['ID']['output'];
interval?: Maybe<Scalars['String']['output']>;
status: Scalars['String']['output']; status: Scalars['String']['output'];
}; };
@@ -263,7 +264,6 @@ export enum FieldMetadataType {
DateTime = 'DATE_TIME', DateTime = 'DATE_TIME',
Email = 'EMAIL', Email = 'EMAIL',
FullName = 'FULL_NAME', FullName = 'FULL_NAME',
Json = 'JSON',
Link = 'LINK', Link = 'LINK',
MultiSelect = 'MULTI_SELECT', MultiSelect = 'MULTI_SELECT',
Number = 'NUMBER', Number = 'NUMBER',
@@ -272,6 +272,7 @@ export enum FieldMetadataType {
Position = 'POSITION', Position = 'POSITION',
Probability = 'PROBABILITY', Probability = 'PROBABILITY',
Rating = 'RATING', Rating = 'RATING',
RawJson = 'RAW_JSON',
Relation = 'RELATION', Relation = 'RELATION',
Select = 'SELECT', Select = 'SELECT',
Text = 'TEXT', Text = 'TEXT',
@@ -341,6 +342,7 @@ export type Mutation = {
renewToken: AuthTokens; renewToken: AuthTokens;
signUp: LoginToken; signUp: LoginToken;
track: Analytics; track: Analytics;
updateBillingSubscription: UpdateBillingEntity;
updateOneField: Field; updateOneField: Field;
updateOneObject: Object; updateOneObject: Object;
updatePasswordViaResetToken: InvalidatePassword; updatePasswordViaResetToken: InvalidatePassword;
@@ -545,6 +547,8 @@ export type Query = {
fields: FieldConnection; fields: FieldConnection;
findWorkspaceFromInviteHash: Workspace; findWorkspaceFromInviteHash: Workspace;
getProductPrices: ProductPricesEntity; getProductPrices: ProductPricesEntity;
getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal;
getTimelineCalendarEventsFromPersonId: TimelineCalendarEventsWithTotal;
getTimelineThreadsFromCompanyId: TimelineThreadsWithTotal; getTimelineThreadsFromCompanyId: TimelineThreadsWithTotal;
getTimelineThreadsFromPersonId: TimelineThreadsWithTotal; getTimelineThreadsFromPersonId: TimelineThreadsWithTotal;
object: Object; 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 = { export type QueryGetTimelineThreadsFromCompanyIdArgs = {
companyId: Scalars['ID']['input']; companyId: Scalars['ID']['input'];
page: Scalars['Int']['input']; page: Scalars['Int']['input'];
@@ -697,7 +715,7 @@ export type Sentry = {
export type SessionEntity = { export type SessionEntity = {
__typename?: 'SessionEntity'; __typename?: 'SessionEntity';
url: Scalars['String']['output']; url?: Maybe<Scalars['String']['output']>;
}; };
/** Sort Directions */ /** Sort Directions */
@@ -724,6 +742,45 @@ export type Telemetry = {
enabled: Scalars['Boolean']['output']; enabled: Scalars['Boolean']['output'];
}; };
export type TimelineCalendarEvent = {
__typename?: 'TimelineCalendarEvent';
attendees: Array<TimelineCalendarEventAttendee>;
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<Scalars['ID']['output']>;
workspaceMemberId?: Maybe<Scalars['ID']['output']>;
};
/** Visibility of the calendar event */
export enum TimelineCalendarEventVisibility {
Metadata = 'METADATA',
ShareEverything = 'SHARE_EVERYTHING'
}
export type TimelineCalendarEventsWithTotal = {
__typename?: 'TimelineCalendarEventsWithTotal';
timelineCalendarEvents: Array<TimelineCalendarEvent>;
totalNumberOfCalendarEvents: Scalars['Int']['output'];
};
export type TimelineThread = { export type TimelineThread = {
__typename?: 'TimelineThread'; __typename?: 'TimelineThread';
firstParticipant: TimelineThreadParticipant; firstParticipant: TimelineThreadParticipant;
@@ -760,6 +817,12 @@ export type TransientToken = {
transientToken: AuthToken; transientToken: AuthToken;
}; };
export type UpdateBillingEntity = {
__typename?: 'UpdateBillingEntity';
/** Boolean that confirms query was successful */
success: Scalars['Boolean']['output'];
};
export type UpdateFieldInput = { export type UpdateFieldInput = {
defaultValue?: InputMaybe<Scalars['JSON']['input']>; defaultValue?: InputMaybe<Scalars['JSON']['input']>;
description?: InputMaybe<Scalars['String']['input']>; description?: InputMaybe<Scalars['String']['input']>;

View File

@@ -184,7 +184,6 @@ export enum FieldMetadataType {
DateTime = 'DATE_TIME', DateTime = 'DATE_TIME',
Email = 'EMAIL', Email = 'EMAIL',
FullName = 'FULL_NAME', FullName = 'FULL_NAME',
Json = 'JSON',
Link = 'LINK', Link = 'LINK',
MultiSelect = 'MULTI_SELECT', MultiSelect = 'MULTI_SELECT',
Number = 'NUMBER', Number = 'NUMBER',
@@ -193,6 +192,7 @@ export enum FieldMetadataType {
Position = 'POSITION', Position = 'POSITION',
Probability = 'PROBABILITY', Probability = 'PROBABILITY',
Rating = 'RATING', Rating = 'RATING',
RawJson = 'RAW_JSON',
Relation = 'RELATION', Relation = 'RELATION',
Select = 'SELECT', Select = 'SELECT',
Text = 'TEXT', Text = 'TEXT',
@@ -255,7 +255,6 @@ export type Mutation = {
generateJWT: AuthTokens; generateJWT: AuthTokens;
generateTransientToken: TransientToken; generateTransientToken: TransientToken;
impersonate: Verify; impersonate: Verify;
removeWorkspaceMember: Scalars['String'];
renewToken: AuthTokens; renewToken: AuthTokens;
signUp: LoginToken; signUp: LoginToken;
track: Analytics; track: Analytics;
@@ -314,11 +313,6 @@ export type MutationImpersonateArgs = {
}; };
export type MutationRemoveWorkspaceMemberArgs = {
memberId: Scalars['String'];
};
export type MutationRenewTokenArgs = { export type MutationRenewTokenArgs = {
refreshToken: Scalars['String']; 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 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<{ export type ActivateWorkspaceMutationVariables = Exact<{
input: ActivateWorkspaceInput; input: ActivateWorkspaceInput;
}>; }>;
@@ -2311,37 +2298,6 @@ export function useGetCurrentUserLazyQuery(baseOptions?: Apollo.LazyQueryHookOpt
export type GetCurrentUserQueryHookResult = ReturnType<typeof useGetCurrentUserQuery>; export type GetCurrentUserQueryHookResult = ReturnType<typeof useGetCurrentUserQuery>;
export type GetCurrentUserLazyQueryHookResult = ReturnType<typeof useGetCurrentUserLazyQuery>; export type GetCurrentUserLazyQueryHookResult = ReturnType<typeof useGetCurrentUserLazyQuery>;
export type GetCurrentUserQueryResult = Apollo.QueryResult<GetCurrentUserQuery, GetCurrentUserQueryVariables>; export type GetCurrentUserQueryResult = Apollo.QueryResult<GetCurrentUserQuery, GetCurrentUserQueryVariables>;
export const RemoveWorkspaceMemberDocument = gql`
mutation RemoveWorkspaceMember($memberId: String!) {
removeWorkspaceMember(memberId: $memberId)
}
`;
export type RemoveWorkspaceMemberMutationFn = Apollo.MutationFunction<RemoveWorkspaceMemberMutation, RemoveWorkspaceMemberMutationVariables>;
/**
* __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<RemoveWorkspaceMemberMutation, RemoveWorkspaceMemberMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<RemoveWorkspaceMemberMutation, RemoveWorkspaceMemberMutationVariables>(RemoveWorkspaceMemberDocument, options);
}
export type RemoveWorkspaceMemberMutationHookResult = ReturnType<typeof useRemoveWorkspaceMemberMutation>;
export type RemoveWorkspaceMemberMutationResult = Apollo.MutationResult<RemoveWorkspaceMemberMutation>;
export type RemoveWorkspaceMemberMutationOptions = Apollo.BaseMutationOptions<RemoveWorkspaceMemberMutation, RemoveWorkspaceMemberMutationVariables>;
export const ActivateWorkspaceDocument = gql` export const ActivateWorkspaceDocument = gql`
mutation ActivateWorkspace($input: ActivateWorkspaceInput!) { mutation ActivateWorkspace($input: ActivateWorkspaceInput!) {
activateWorkspace(data: $input) { activateWorkspace(data: $input) {

View File

@@ -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) => <EventRow key={event.id} event={event} />)}
</>
);
};

View File

@@ -0,0 +1,11 @@
import { Event } from '@/activities/events/types/Event';
export const EventRow = ({ event }: { event: Event }) => {
return (
<>
<p>
{event.name}:<pre>{event.properties}</pre>
</p>
</>
);
};

View File

@@ -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 <div>No log yet</div>;
}
return (
<EventList
targetableObject={targetableObject}
title="All"
events={events ?? []}
/>
);
};

View File

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

View File

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

View File

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

View File

@@ -8,6 +8,7 @@ export enum CoreObjectNameSingular {
Comment = 'comment', Comment = 'comment',
Company = 'company', Company = 'company',
ConnectedAccount = 'connectedAccount', ConnectedAccount = 'connectedAccount',
Event = 'event',
Favorite = 'favorite', Favorite = 'favorite',
Message = 'message', Message = 'message',
MessageChannel = 'messageChannel', MessageChannel = 'messageChannel',

View File

@@ -34,6 +34,7 @@ export const mapFieldMetadataToGraphQLQuery = ({
'RATING', 'RATING',
'SELECT', 'SELECT',
'POSITION', 'POSITION',
'RAW_JSON',
] as FieldMetadataType[] ] as FieldMetadataType[]
).includes(fieldType); ).includes(fieldType);

View File

@@ -69,6 +69,11 @@ export type FieldRatingMetadata = {
fieldName: string; fieldName: string;
}; };
export type FieldRawJsonMetadata = {
objectMetadataNameSingular?: string;
fieldName: string;
};
export type FieldDefinitionRelationType = export type FieldDefinitionRelationType =
| 'FROM_MANY_OBJECTS' | 'FROM_MANY_OBJECTS'
| 'FROM_ONE_OBJECT' | 'FROM_ONE_OBJECT'

View File

@@ -17,4 +17,5 @@ export type FieldType =
| 'URL' | 'URL'
| 'UUID' | 'UUID'
| 'MULTI_SELECT' | 'MULTI_SELECT'
| 'NUMERIC'; | 'NUMERIC'
| 'RAW_JSON';

View File

@@ -10,6 +10,7 @@ import {
FieldNumberMetadata, FieldNumberMetadata,
FieldPhoneMetadata, FieldPhoneMetadata,
FieldRatingMetadata, FieldRatingMetadata,
FieldRawJsonMetadata,
FieldRelationMetadata, FieldRelationMetadata,
FieldSelectMetadata, FieldSelectMetadata,
FieldTextMetadata, FieldTextMetadata,
@@ -47,6 +48,8 @@ type AssertFieldMetadataFunction = <
? FieldTextMetadata ? FieldTextMetadata
: E extends 'UUID' : E extends 'UUID'
? FieldUuidMetadata ? FieldUuidMetadata
: E extends 'RAW_JSON'
? FieldRawJsonMetadata
: never, : never,
>( >(
fieldType: E, fieldType: E,

View File

@@ -0,0 +1,6 @@
import { FieldDefinition } from '../FieldDefinition';
import { FieldMetadata, FieldRawJsonMetadata } from '../FieldMetadata';
export const isFieldRawJson = (
field: Pick<FieldDefinition<FieldMetadata>, 'type'>,
): field is FieldDefinition<FieldRawJsonMetadata> => field.type === 'RAW_JSON';

View File

@@ -3,6 +3,7 @@ import { useRecoilValue } from 'recoil';
import { Calendar } from '@/activities/calendar/components/Calendar'; import { Calendar } from '@/activities/calendar/components/Calendar';
import { EmailThreads } from '@/activities/emails/components/EmailThreads'; import { EmailThreads } from '@/activities/emails/components/EmailThreads';
import { Events } from '@/activities/events/components/Events';
import { Attachments } from '@/activities/files/components/Attachments'; import { Attachments } from '@/activities/files/components/Attachments';
import { Notes } from '@/activities/notes/components/Notes'; import { Notes } from '@/activities/notes/components/Notes';
import { ObjectTasks } from '@/activities/tasks/components/ObjectTasks'; import { ObjectTasks } from '@/activities/tasks/components/ObjectTasks';
@@ -65,6 +66,8 @@ export const ShowPageRightContainer = ({
const activeTabId = useRecoilValue(activeTabIdState); const activeTabId = useRecoilValue(activeTabIdState);
const shouldDisplayCalendarTab = useIsFeatureEnabled('IS_CALENDAR_ENABLED'); const shouldDisplayCalendarTab = useIsFeatureEnabled('IS_CALENDAR_ENABLED');
const shouldDisplayLogTab = useIsFeatureEnabled('IS_EVENT_OBJECT_ENABLED');
const shouldDisplayEmailsTab = const shouldDisplayEmailsTab =
(emails && (emails &&
targetableObject.targetObjectNameSingular === targetableObject.targetObjectNameSingular ===
@@ -101,7 +104,6 @@ export const ShowPageRightContainer = ({
title: 'Emails', title: 'Emails',
Icon: IconMail, Icon: IconMail,
hide: !shouldDisplayEmailsTab, hide: !shouldDisplayEmailsTab,
hasBetaPill: true,
}, },
{ {
id: 'calendar', id: 'calendar',
@@ -109,6 +111,13 @@ export const ShowPageRightContainer = ({
Icon: IconCalendarEvent, Icon: IconCalendarEvent,
hide: !shouldDisplayCalendarTab, hide: !shouldDisplayCalendarTab,
}, },
{
id: 'logs',
title: 'Logs',
Icon: IconTimelineEvent,
hide: !shouldDisplayLogTab,
hasBetaPill: true,
},
]; ];
return ( return (
@@ -131,6 +140,7 @@ export const ShowPageRightContainer = ({
)} )}
{activeTabId === 'emails' && <EmailThreads entity={targetableObject} />} {activeTabId === 'emails' && <EmailThreads entity={targetableObject} />}
{activeTabId === 'calendar' && <Calendar />} {activeTabId === 'calendar' && <Calendar />}
{activeTabId === 'logs' && <Events targetableObject={targetableObject} />}
</StyledShowPageRightContainer> </StyledShowPageRightContainer>
); );
}; };

View File

@@ -2,12 +2,16 @@ import { Injectable } from '@nestjs/common';
import { MessageQueueJob } from 'src/engine/integrations/message-queue/interfaces/message-queue-job.interface'; 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 { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; 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 = { export type SaveEventToDbJobData = {
workspaceId: string; workspaceId: string;
recordId: string; recordId: string;
userId: string | undefined;
objectName: string; objectName: string;
operation: string; operation: string;
details: any; details: any;
@@ -16,39 +20,47 @@ export type SaveEventToDbJobData = {
@Injectable() @Injectable()
export class SaveEventToDbJob implements MessageQueueJob<SaveEventToDbJobData> { export class SaveEventToDbJob implements MessageQueueJob<SaveEventToDbJobData> {
constructor( constructor(
private readonly dataSourceService: DataSourceService, @InjectObjectMetadataRepository(WorkspaceMemberObjectMetadata)
private readonly workspaceDataSourceService: WorkspaceDataSourceService, 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<void> { async handle(data: SaveEventToDbJobData): Promise<void> {
const dataSourceMetadata = let workspaceMemberId: string | null = null;
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
data.workspaceId, if (data.userId) {
); const workspaceMember = await this.workspaceMemberService.getByIdOrFail(
const workspaceDataSource = data.userId,
await this.workspaceDataSourceService.connectToWorkspaceDataSource(
data.workspaceId, data.workspaceId,
); );
const eventType = `${data.operation}.${data.objectName}`; workspaceMemberId = workspaceMember.id;
}
// 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 ( if (
data.objectName != 'person' && data.objectName != 'person' &&
data.objectName != 'company' && data.objectName != 'company' &&
data.objectName != 'opportunities' data.objectName != 'opportunity'
) { ) {
return; return;
} }
await workspaceDataSource?.query( if (data.details.diff) {
`INSERT INTO ${dataSourceMetadata.schema}."event" // we remove "before" and "after" property for a cleaner/slimmer event payload
("name", "properties", "${data.objectName}Id") data.details = {
VALUES ('${eventType}', '${JSON.stringify(data.details)}', '${ diff: data.details.diff,
data.recordId };
}') RETURNING *`, }
await this.eventService.insert(
`${data.operation}.${data.objectName}`,
data.details,
workspaceMemberId,
data.objectName,
data.recordId,
data.workspaceId,
); );
} }
} }

View File

@@ -15,6 +15,8 @@ import {
FeatureFlagEntity, FeatureFlagEntity,
FeatureFlagKeys, FeatureFlagKeys,
} from 'src/engine/core-modules/feature-flag/feature-flag.entity'; } 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() @Injectable()
export class EntityEventsToDbListener { export class EntityEventsToDbListener {
@@ -31,7 +33,12 @@ export class EntityEventsToDbListener {
} }
@OnEvent('*.updated') @OnEvent('*.updated')
async handleUpdate(payload: ObjectRecordCreateEvent<any>) { async handleUpdate(payload: ObjectRecordUpdateEvent<any>) {
payload.details.diff = objectRecordChangedValues(
payload.details.before,
payload.details.after,
);
return this.handle(payload, 'updated'); return this.handle(payload, 'updated');
} }
@@ -58,6 +65,7 @@ export class EntityEventsToDbListener {
this.messageQueueService.add<SaveEventToDbJobData>(SaveEventToDbJob.name, { this.messageQueueService.add<SaveEventToDbJobData>(SaveEventToDbJob.name, {
workspaceId: payload.workspaceId, workspaceId: payload.workspaceId,
userId: payload.userId,
recordId: payload.recordId, recordId: payload.recordId,
objectName: payload.objectMetadata.nameSingular, objectName: payload.objectMetadata.nameSingular,
operation: operation, operation: operation,

View File

@@ -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 { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.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'; import { WorkspaceQueryRunnerService } from './workspace-query-runner.service';
@@ -21,6 +24,10 @@ import { EntityEventsToDbListener } from './listeners/entity-events-to-db.listen
WorkspaceDataSourceModule, WorkspaceDataSourceModule,
WorkspacePreQueryHookModule, WorkspacePreQueryHookModule,
TypeOrmModule.forFeature([Workspace, FeatureFlagEntity], 'core'), TypeOrmModule.forFeature([Workspace, FeatureFlagEntity], 'core'),
ObjectMetadataRepositoryModule.forFeature([
WorkspaceMemberObjectMetadata,
EventObjectMetadata,
]),
], ],
providers: [ providers: [
WorkspaceQueryRunnerService, WorkspaceQueryRunnerService,

View File

@@ -216,7 +216,7 @@ export class WorkspaceQueryRunnerService {
args: CreateManyResolverArgs<Record>, args: CreateManyResolverArgs<Record>,
options: WorkspaceQueryRunnerOptions, options: WorkspaceQueryRunnerOptions,
): Promise<Record[] | undefined> { ): Promise<Record[] | undefined> {
const { workspaceId, objectMetadataItem } = options; const { workspaceId, userId, objectMetadataItem } = options;
const computedArgs = await this.queryRunnerArgsFactory.create( const computedArgs = await this.queryRunnerArgsFactory.create(
args, args,
options, options,
@@ -246,6 +246,7 @@ export class WorkspaceQueryRunnerService {
parsedResults.forEach((record) => { parsedResults.forEach((record) => {
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.created`, { this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.created`, {
workspaceId, workspaceId,
userId,
recordId: record.id, recordId: record.id,
objectMetadata: objectMetadataItem, objectMetadata: objectMetadataItem,
details: { details: {
@@ -270,7 +271,7 @@ export class WorkspaceQueryRunnerService {
args: UpdateOneResolverArgs<Record>, args: UpdateOneResolverArgs<Record>,
options: WorkspaceQueryRunnerOptions, options: WorkspaceQueryRunnerOptions,
): Promise<Record | undefined> { ): Promise<Record | undefined> {
const { workspaceId, objectMetadataItem } = options; const { workspaceId, userId, objectMetadataItem } = options;
const existingRecord = await this.findOne( const existingRecord = await this.findOne(
{ filter: { id: { eq: args.id } } } as FindOneResolverArgs, { filter: { id: { eq: args.id } } } as FindOneResolverArgs,
@@ -300,6 +301,7 @@ export class WorkspaceQueryRunnerService {
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.updated`, { this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.updated`, {
workspaceId, workspaceId,
userId,
recordId: (existingRecord as Record).id, recordId: (existingRecord as Record).id,
objectMetadata: objectMetadataItem, objectMetadata: objectMetadataItem,
details: { details: {
@@ -356,7 +358,7 @@ export class WorkspaceQueryRunnerService {
args: DeleteManyResolverArgs<Filter>, args: DeleteManyResolverArgs<Filter>,
options: WorkspaceQueryRunnerOptions, options: WorkspaceQueryRunnerOptions,
): Promise<Record[] | undefined> { ): Promise<Record[] | undefined> {
const { workspaceId, objectMetadataItem } = options; const { workspaceId, userId, objectMetadataItem } = options;
const maximumRecordAffected = this.environmentService.get( const maximumRecordAffected = this.environmentService.get(
'MUTATION_MAXIMUM_RECORD_AFFECTED', 'MUTATION_MAXIMUM_RECORD_AFFECTED',
); );
@@ -384,6 +386,7 @@ export class WorkspaceQueryRunnerService {
parsedResults.forEach((record) => { parsedResults.forEach((record) => {
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.deleted`, { this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.deleted`, {
workspaceId, workspaceId,
userId,
recordId: record.id, recordId: record.id,
objectMetadata: objectMetadataItem, objectMetadata: objectMetadataItem,
details: { details: {
@@ -399,7 +402,7 @@ export class WorkspaceQueryRunnerService {
args: DeleteOneResolverArgs, args: DeleteOneResolverArgs,
options: WorkspaceQueryRunnerOptions, options: WorkspaceQueryRunnerOptions,
): Promise<Record | undefined> { ): Promise<Record | undefined> {
const { workspaceId, objectMetadataItem } = options; const { workspaceId, userId, objectMetadataItem } = options;
const query = await this.workspaceQueryBuilderFactory.deleteOne( const query = await this.workspaceQueryBuilderFactory.deleteOne(
args, args,
options, options,
@@ -422,6 +425,7 @@ export class WorkspaceQueryRunnerService {
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.deleted`, { this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.deleted`, {
workspaceId, workspaceId,
userId,
recordId: args.id, recordId: args.id,
objectMetadata: objectMetadataItem, objectMetadata: objectMetadataItem,
details: { details: {

View File

@@ -8,4 +8,4 @@ export * from './string-filter.input-type';
export * from './time-filter.input-type'; export * from './time-filter.input-type';
export * from './uuid-filter.input-type'; export * from './uuid-filter.input-type';
export * from './boolean-filter.input-type'; export * from './boolean-filter.input-type';
export * from './json-filter.input-type'; export * from './raw-json-filter.input-type';

View File

@@ -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'; import { FilterIs } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/filter-is.input-type';
export const JsonFilterType = new GraphQLInputObjectType({ export const RawJsonFilterType = new GraphQLInputObjectType({
name: 'JsonFilter', name: 'RawJsonFilter',
fields: { fields: {
is: { type: FilterIs }, is: { type: FilterIs },
}, },

View File

@@ -32,7 +32,7 @@ import {
IntFilterType, IntFilterType,
BooleanFilterType, BooleanFilterType,
BigFloatFilterType, BigFloatFilterType,
JsonFilterType, RawJsonFilterType,
} from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input'; } 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 { 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'; import { BigFloatScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@@ -70,7 +70,7 @@ export class TypeMapperService {
[FieldMetadataType.PROBABILITY, GraphQLFloat], [FieldMetadataType.PROBABILITY, GraphQLFloat],
[FieldMetadataType.RELATION, GraphQLID], [FieldMetadataType.RELATION, GraphQLID],
[FieldMetadataType.POSITION, PositionScalarType], [FieldMetadataType.POSITION, PositionScalarType],
[FieldMetadataType.JSON, GraphQLJSON], [FieldMetadataType.RAW_JSON, GraphQLJSON],
]); ]);
return typeScalarMapping.get(fieldMetadataType); return typeScalarMapping.get(fieldMetadataType);
@@ -102,7 +102,7 @@ export class TypeMapperService {
[FieldMetadataType.PROBABILITY, FloatFilterType], [FieldMetadataType.PROBABILITY, FloatFilterType],
[FieldMetadataType.RELATION, UUIDFilterType], [FieldMetadataType.RELATION, UUIDFilterType],
[FieldMetadataType.POSITION, FloatFilterType], [FieldMetadataType.POSITION, FloatFilterType],
[FieldMetadataType.JSON, JsonFilterType], [FieldMetadataType.RAW_JSON, RawJsonFilterType],
]); ]);
return typeFilterMapping.get(fieldMetadataType); return typeFilterMapping.get(fieldMetadataType);
@@ -126,7 +126,7 @@ export class TypeMapperService {
[FieldMetadataType.SELECT, OrderByDirectionType], [FieldMetadataType.SELECT, OrderByDirectionType],
[FieldMetadataType.MULTI_SELECT, OrderByDirectionType], [FieldMetadataType.MULTI_SELECT, OrderByDirectionType],
[FieldMetadataType.POSITION, OrderByDirectionType], [FieldMetadataType.POSITION, OrderByDirectionType],
[FieldMetadataType.JSON, OrderByDirectionType], [FieldMetadataType.RAW_JSON, OrderByDirectionType],
]); ]);
return typeOrderByMapping.get(fieldMetadataType); return typeOrderByMapping.get(fieldMetadataType);

View File

@@ -69,7 +69,7 @@ const getSchemaComponentsProperties = (
), ),
}; };
break; break;
case FieldMetadataType.JSON: case FieldMetadataType.RAW_JSON:
type: 'object'; type: 'object';
break; break;
default: default:

View File

@@ -4,5 +4,6 @@ export class ObjectRecordUpdateEvent<T> extends ObjectRecordBaseEvent {
details: { details: {
before: T; before: T;
after: T; after: T;
diff?: Partial<T>;
}; };
} }

View File

@@ -3,6 +3,7 @@ import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metad
export class ObjectRecordBaseEvent { export class ObjectRecordBaseEvent {
workspaceId: string; workspaceId: string;
recordId: string; recordId: string;
userId?: string;
objectMetadata: ObjectMetadataInterface; objectMetadata: ObjectMetadataInterface;
details: any; details: any;
} }

View File

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

View File

@@ -0,0 +1,28 @@
import deepEqual from 'deep-equal';
export const objectRecordChangedValues = (
oldRecord: Record<string, any>,
newRecord: Record<string, any>,
) => {
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<string, { before: any; after: any }>,
);
return changedValues;
};

View File

@@ -17,7 +17,7 @@ export class FieldMetadataDefaultValueString {
value: string | null; value: string | null;
} }
export class FieldMetadataDefaultValueJson { export class FieldMetadataDefaultValueRawJson {
@ValidateIf((_object, value) => value !== null) @ValidateIf((_object, value) => value !== null)
@IsJSON() @IsJSON()
value: JSON | null; value: JSON | null;

View File

@@ -36,7 +36,7 @@ export enum FieldMetadataType {
MULTI_SELECT = 'MULTI_SELECT', MULTI_SELECT = 'MULTI_SELECT',
RELATION = 'RELATION', RELATION = 'RELATION',
POSITION = 'POSITION', POSITION = 'POSITION',
JSON = 'JSON', RAW_JSON = 'RAW_JSON',
} }
@Entity('fieldMetadata') @Entity('fieldMetadata')

View File

@@ -3,7 +3,7 @@ import {
FieldMetadataDefaultValueCurrency, FieldMetadataDefaultValueCurrency,
FieldMetadataDefaultValueDateTime, FieldMetadataDefaultValueDateTime,
FieldMetadataDefaultValueFullName, FieldMetadataDefaultValueFullName,
FieldMetadataDefaultValueJson, FieldMetadataDefaultValueRawJson,
FieldMetadataDefaultValueLink, FieldMetadataDefaultValueLink,
FieldMetadataDefaultValueNumber, FieldMetadataDefaultValueNumber,
FieldMetadataDefaultValueString, FieldMetadataDefaultValueString,
@@ -51,7 +51,7 @@ type FieldMetadataDefaultValueMapping = {
[FieldMetadataType.RATING]: FieldMetadataDefaultValueString; [FieldMetadataType.RATING]: FieldMetadataDefaultValueString;
[FieldMetadataType.SELECT]: FieldMetadataDefaultValueString; [FieldMetadataType.SELECT]: FieldMetadataDefaultValueString;
[FieldMetadataType.MULTI_SELECT]: FieldMetadataDefaultValueStringArray; [FieldMetadataType.MULTI_SELECT]: FieldMetadataDefaultValueStringArray;
[FieldMetadataType.JSON]: FieldMetadataDefaultValueJson; [FieldMetadataType.RAW_JSON]: FieldMetadataDefaultValueRawJson;
}; };
type DefaultValueByFieldMetadata<T extends FieldMetadataType | 'default'> = [ type DefaultValueByFieldMetadata<T extends FieldMetadataType | 'default'> = [

View File

@@ -35,7 +35,7 @@ export function generateTargetColumnMap(
case FieldMetadataType.SELECT: case FieldMetadataType.SELECT:
case FieldMetadataType.MULTI_SELECT: case FieldMetadataType.MULTI_SELECT:
case FieldMetadataType.POSITION: case FieldMetadataType.POSITION:
case FieldMetadataType.JSON: case FieldMetadataType.RAW_JSON:
return { return {
value: columnName, value: columnName,
}; };

View File

@@ -9,7 +9,7 @@ import {
FieldMetadataDefaultValueCurrency, FieldMetadataDefaultValueCurrency,
FieldMetadataDefaultValueDateTime, FieldMetadataDefaultValueDateTime,
FieldMetadataDefaultValueFullName, FieldMetadataDefaultValueFullName,
FieldMetadataDefaultValueJson, FieldMetadataDefaultValueRawJson,
FieldMetadataDefaultValueLink, FieldMetadataDefaultValueLink,
FieldMetadataDefaultValueNumber, FieldMetadataDefaultValueNumber,
FieldMetadataDefaultValueString, FieldMetadataDefaultValueString,
@@ -40,7 +40,7 @@ export const defaultValueValidatorsMap = {
[FieldMetadataType.RATING]: [FieldMetadataDefaultValueString], [FieldMetadataType.RATING]: [FieldMetadataDefaultValueString],
[FieldMetadataType.SELECT]: [FieldMetadataDefaultValueString], [FieldMetadataType.SELECT]: [FieldMetadataDefaultValueString],
[FieldMetadataType.MULTI_SELECT]: [FieldMetadataDefaultValueStringArray], [FieldMetadataType.MULTI_SELECT]: [FieldMetadataDefaultValueStringArray],
[FieldMetadataType.JSON]: [FieldMetadataDefaultValueJson], [FieldMetadataType.RAW_JSON]: [FieldMetadataDefaultValueRawJson],
}; };
export const validateDefaultValueForType = ( export const validateDefaultValueForType = (

View File

@@ -29,7 +29,7 @@ export const fieldMetadataTypeToColumnType = <Type extends FieldMetadataType>(
case FieldMetadataType.SELECT: case FieldMetadataType.SELECT:
case FieldMetadataType.MULTI_SELECT: case FieldMetadataType.MULTI_SELECT:
return 'enum'; return 'enum';
case FieldMetadataType.JSON: case FieldMetadataType.RAW_JSON:
return 'jsonb'; return 'jsonb';
default: default:
throw new Error(`Cannot convert ${fieldMetadataType} to column type.`); throw new Error(`Cannot convert ${fieldMetadataType} to column type.`);

View File

@@ -67,7 +67,7 @@ export class WorkspaceMigrationFactory {
[FieldMetadataType.NUMERIC, { factory: this.basicColumnActionFactory }], [FieldMetadataType.NUMERIC, { factory: this.basicColumnActionFactory }],
[FieldMetadataType.NUMBER, { factory: this.basicColumnActionFactory }], [FieldMetadataType.NUMBER, { factory: this.basicColumnActionFactory }],
[FieldMetadataType.POSITION, { factory: this.basicColumnActionFactory }], [FieldMetadataType.POSITION, { factory: this.basicColumnActionFactory }],
[FieldMetadataType.JSON, { factory: this.basicColumnActionFactory }], [FieldMetadataType.RAW_JSON, { factory: this.basicColumnActionFactory }],
[ [
FieldMetadataType.PROBABILITY, FieldMetadataType.PROBABILITY,
{ factory: this.basicColumnActionFactory }, { factory: this.basicColumnActionFactory },

View File

@@ -5,6 +5,7 @@ import { CalendarEventRepository } from 'src/modules/calendar/repositories/calen
import { CompanyRepository } from 'src/modules/company/repositories/company.repository'; import { CompanyRepository } from 'src/modules/company/repositories/company.repository';
import { BlocklistRepository } from 'src/modules/connected-account/repositories/blocklist.repository'; import { BlocklistRepository } from 'src/modules/connected-account/repositories/blocklist.repository';
import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.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 { MessageChannelMessageAssociationRepository } from 'src/modules/messaging/repositories/message-channel-message-association.repository';
import { MessageChannelRepository } from 'src/modules/messaging/repositories/message-channel.repository'; import { MessageChannelRepository } from 'src/modules/messaging/repositories/message-channel.repository';
import { MessageParticipantRepository } from 'src/modules/messaging/repositories/message-participant.repository'; import { MessageParticipantRepository } from 'src/modules/messaging/repositories/message-participant.repository';
@@ -22,6 +23,7 @@ export const metadataToRepositoryMapping = {
CalendarEventObjectMetadata: CalendarEventRepository, CalendarEventObjectMetadata: CalendarEventRepository,
CompanyObjectMetadata: CompanyRepository, CompanyObjectMetadata: CompanyRepository,
ConnectedAccountObjectMetadata: ConnectedAccountRepository, ConnectedAccountObjectMetadata: ConnectedAccountRepository,
EventObjectMetadata: EventRepository,
MessageChannelMessageAssociationObjectMetadata: MessageChannelMessageAssociationObjectMetadata:
MessageChannelMessageAssociationRepository, MessageChannelMessageAssociationRepository,
MessageChannelObjectMetadata: MessageChannelRepository, MessageChannelObjectMetadata: MessageChannelRepository,

View File

@@ -22,7 +22,7 @@ export const mapFieldMetadataTypeToDataType = (
return 'boolean'; return 'boolean';
case FieldMetadataType.DATE_TIME: case FieldMetadataType.DATE_TIME:
return 'timestamp'; return 'timestamp';
case FieldMetadataType.JSON: case FieldMetadataType.RAW_JSON:
return 'jsonb'; return 'jsonb';
case FieldMetadataType.RATING: case FieldMetadataType.RATING:
case FieldMetadataType.SELECT: case FieldMetadataType.SELECT:

View File

@@ -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<void> {
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,
);
}
}

View File

@@ -39,7 +39,7 @@ export class EventObjectMetadata extends BaseObjectMetadata {
@FieldMetadata({ @FieldMetadata({
standardId: eventStandardFieldIds.properties, standardId: eventStandardFieldIds.properties,
type: FieldMetadataType.JSON, type: FieldMetadataType.RAW_JSON,
label: 'Event details', label: 'Event details',
description: 'Json value for event details', description: 'Json value for event details',
icon: 'IconListDetails', icon: 'IconListDetails',