mirror of
https://github.com/lingble/twenty.git
synced 2025-11-02 21:57:56 +00:00
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:
@@ -73,6 +73,7 @@ export type Billing = {
|
||||
export type BillingSubscription = {
|
||||
__typename?: 'BillingSubscription';
|
||||
id: Scalars['ID']['output'];
|
||||
interval?: Maybe<Scalars['String']['output']>;
|
||||
status: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
@@ -263,7 +264,6 @@ export enum FieldMetadataType {
|
||||
DateTime = 'DATE_TIME',
|
||||
Email = 'EMAIL',
|
||||
FullName = 'FULL_NAME',
|
||||
Json = 'JSON',
|
||||
Link = 'LINK',
|
||||
MultiSelect = 'MULTI_SELECT',
|
||||
Number = 'NUMBER',
|
||||
@@ -272,6 +272,7 @@ export enum FieldMetadataType {
|
||||
Position = 'POSITION',
|
||||
Probability = 'PROBABILITY',
|
||||
Rating = 'RATING',
|
||||
RawJson = 'RAW_JSON',
|
||||
Relation = 'RELATION',
|
||||
Select = 'SELECT',
|
||||
Text = 'TEXT',
|
||||
@@ -341,6 +342,7 @@ export type Mutation = {
|
||||
renewToken: AuthTokens;
|
||||
signUp: LoginToken;
|
||||
track: Analytics;
|
||||
updateBillingSubscription: UpdateBillingEntity;
|
||||
updateOneField: Field;
|
||||
updateOneObject: Object;
|
||||
updatePasswordViaResetToken: InvalidatePassword;
|
||||
@@ -545,6 +547,8 @@ export type Query = {
|
||||
fields: FieldConnection;
|
||||
findWorkspaceFromInviteHash: Workspace;
|
||||
getProductPrices: ProductPricesEntity;
|
||||
getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal;
|
||||
getTimelineCalendarEventsFromPersonId: TimelineCalendarEventsWithTotal;
|
||||
getTimelineThreadsFromCompanyId: TimelineThreadsWithTotal;
|
||||
getTimelineThreadsFromPersonId: TimelineThreadsWithTotal;
|
||||
object: Object;
|
||||
@@ -591,6 +595,20 @@ export type QueryGetProductPricesArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryGetTimelineCalendarEventsFromCompanyIdArgs = {
|
||||
companyId: Scalars['ID']['input'];
|
||||
page: Scalars['Int']['input'];
|
||||
pageSize: Scalars['Int']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryGetTimelineCalendarEventsFromPersonIdArgs = {
|
||||
page: Scalars['Int']['input'];
|
||||
pageSize: Scalars['Int']['input'];
|
||||
personId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryGetTimelineThreadsFromCompanyIdArgs = {
|
||||
companyId: Scalars['ID']['input'];
|
||||
page: Scalars['Int']['input'];
|
||||
@@ -697,7 +715,7 @@ export type Sentry = {
|
||||
|
||||
export type SessionEntity = {
|
||||
__typename?: 'SessionEntity';
|
||||
url: Scalars['String']['output'];
|
||||
url?: Maybe<Scalars['String']['output']>;
|
||||
};
|
||||
|
||||
/** Sort Directions */
|
||||
@@ -724,6 +742,45 @@ export type Telemetry = {
|
||||
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 = {
|
||||
__typename?: 'TimelineThread';
|
||||
firstParticipant: TimelineThreadParticipant;
|
||||
@@ -760,6 +817,12 @@ export type TransientToken = {
|
||||
transientToken: AuthToken;
|
||||
};
|
||||
|
||||
export type UpdateBillingEntity = {
|
||||
__typename?: 'UpdateBillingEntity';
|
||||
/** Boolean that confirms query was successful */
|
||||
success: Scalars['Boolean']['output'];
|
||||
};
|
||||
|
||||
export type UpdateFieldInput = {
|
||||
defaultValue?: InputMaybe<Scalars['JSON']['input']>;
|
||||
description?: InputMaybe<Scalars['String']['input']>;
|
||||
|
||||
@@ -184,7 +184,6 @@ export enum FieldMetadataType {
|
||||
DateTime = 'DATE_TIME',
|
||||
Email = 'EMAIL',
|
||||
FullName = 'FULL_NAME',
|
||||
Json = 'JSON',
|
||||
Link = 'LINK',
|
||||
MultiSelect = 'MULTI_SELECT',
|
||||
Number = 'NUMBER',
|
||||
@@ -193,6 +192,7 @@ export enum FieldMetadataType {
|
||||
Position = 'POSITION',
|
||||
Probability = 'PROBABILITY',
|
||||
Rating = 'RATING',
|
||||
RawJson = 'RAW_JSON',
|
||||
Relation = 'RELATION',
|
||||
Select = 'SELECT',
|
||||
Text = 'TEXT',
|
||||
@@ -255,7 +255,6 @@ export type Mutation = {
|
||||
generateJWT: AuthTokens;
|
||||
generateTransientToken: TransientToken;
|
||||
impersonate: Verify;
|
||||
removeWorkspaceMember: Scalars['String'];
|
||||
renewToken: AuthTokens;
|
||||
signUp: LoginToken;
|
||||
track: Analytics;
|
||||
@@ -314,11 +313,6 @@ export type MutationImpersonateArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationRemoveWorkspaceMemberArgs = {
|
||||
memberId: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationRenewTokenArgs = {
|
||||
refreshToken: Scalars['String'];
|
||||
};
|
||||
@@ -1098,13 +1092,6 @@ export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', status: string, interval?: string | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null } | null }> } };
|
||||
|
||||
export type RemoveWorkspaceMemberMutationVariables = Exact<{
|
||||
memberId: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type RemoveWorkspaceMemberMutation = { __typename?: 'Mutation', removeWorkspaceMember: string };
|
||||
|
||||
export type ActivateWorkspaceMutationVariables = Exact<{
|
||||
input: ActivateWorkspaceInput;
|
||||
}>;
|
||||
@@ -2311,37 +2298,6 @@ export function useGetCurrentUserLazyQuery(baseOptions?: Apollo.LazyQueryHookOpt
|
||||
export type GetCurrentUserQueryHookResult = ReturnType<typeof useGetCurrentUserQuery>;
|
||||
export type GetCurrentUserLazyQueryHookResult = ReturnType<typeof useGetCurrentUserLazyQuery>;
|
||||
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`
|
||||
mutation ActivateWorkspace($input: ActivateWorkspaceInput!) {
|
||||
activateWorkspace(data: $input) {
|
||||
|
||||
@@ -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} />)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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 ?? []}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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[],
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -8,6 +8,7 @@ export enum CoreObjectNameSingular {
|
||||
Comment = 'comment',
|
||||
Company = 'company',
|
||||
ConnectedAccount = 'connectedAccount',
|
||||
Event = 'event',
|
||||
Favorite = 'favorite',
|
||||
Message = 'message',
|
||||
MessageChannel = 'messageChannel',
|
||||
|
||||
@@ -34,6 +34,7 @@ export const mapFieldMetadataToGraphQLQuery = ({
|
||||
'RATING',
|
||||
'SELECT',
|
||||
'POSITION',
|
||||
'RAW_JSON',
|
||||
] as FieldMetadataType[]
|
||||
).includes(fieldType);
|
||||
|
||||
|
||||
@@ -69,6 +69,11 @@ export type FieldRatingMetadata = {
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export type FieldRawJsonMetadata = {
|
||||
objectMetadataNameSingular?: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export type FieldDefinitionRelationType =
|
||||
| 'FROM_MANY_OBJECTS'
|
||||
| 'FROM_ONE_OBJECT'
|
||||
|
||||
@@ -17,4 +17,5 @@ export type FieldType =
|
||||
| 'URL'
|
||||
| 'UUID'
|
||||
| 'MULTI_SELECT'
|
||||
| 'NUMERIC';
|
||||
| 'NUMERIC'
|
||||
| 'RAW_JSON';
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
FieldNumberMetadata,
|
||||
FieldPhoneMetadata,
|
||||
FieldRatingMetadata,
|
||||
FieldRawJsonMetadata,
|
||||
FieldRelationMetadata,
|
||||
FieldSelectMetadata,
|
||||
FieldTextMetadata,
|
||||
@@ -47,6 +48,8 @@ type AssertFieldMetadataFunction = <
|
||||
? FieldTextMetadata
|
||||
: E extends 'UUID'
|
||||
? FieldUuidMetadata
|
||||
: E extends 'RAW_JSON'
|
||||
? FieldRawJsonMetadata
|
||||
: never,
|
||||
>(
|
||||
fieldType: E,
|
||||
|
||||
@@ -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';
|
||||
@@ -3,6 +3,7 @@ import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { Calendar } from '@/activities/calendar/components/Calendar';
|
||||
import { EmailThreads } from '@/activities/emails/components/EmailThreads';
|
||||
import { Events } from '@/activities/events/components/Events';
|
||||
import { Attachments } from '@/activities/files/components/Attachments';
|
||||
import { Notes } from '@/activities/notes/components/Notes';
|
||||
import { ObjectTasks } from '@/activities/tasks/components/ObjectTasks';
|
||||
@@ -65,6 +66,8 @@ export const ShowPageRightContainer = ({
|
||||
const activeTabId = useRecoilValue(activeTabIdState);
|
||||
|
||||
const shouldDisplayCalendarTab = useIsFeatureEnabled('IS_CALENDAR_ENABLED');
|
||||
const shouldDisplayLogTab = useIsFeatureEnabled('IS_EVENT_OBJECT_ENABLED');
|
||||
|
||||
const shouldDisplayEmailsTab =
|
||||
(emails &&
|
||||
targetableObject.targetObjectNameSingular ===
|
||||
@@ -101,7 +104,6 @@ export const ShowPageRightContainer = ({
|
||||
title: 'Emails',
|
||||
Icon: IconMail,
|
||||
hide: !shouldDisplayEmailsTab,
|
||||
hasBetaPill: true,
|
||||
},
|
||||
{
|
||||
id: 'calendar',
|
||||
@@ -109,6 +111,13 @@ export const ShowPageRightContainer = ({
|
||||
Icon: IconCalendarEvent,
|
||||
hide: !shouldDisplayCalendarTab,
|
||||
},
|
||||
{
|
||||
id: 'logs',
|
||||
title: 'Logs',
|
||||
Icon: IconTimelineEvent,
|
||||
hide: !shouldDisplayLogTab,
|
||||
hasBetaPill: true,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -131,6 +140,7 @@ export const ShowPageRightContainer = ({
|
||||
)}
|
||||
{activeTabId === 'emails' && <EmailThreads entity={targetableObject} />}
|
||||
{activeTabId === 'calendar' && <Calendar />}
|
||||
{activeTabId === 'logs' && <Events targetableObject={targetableObject} />}
|
||||
</StyledShowPageRightContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,12 +2,16 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { MessageQueueJob } from 'src/engine/integrations/message-queue/interfaces/message-queue-job.interface';
|
||||
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||
import { EventRepository } from 'src/modules/event/repositiories/event.repository';
|
||||
import { EventObjectMetadata } from 'src/modules/event/standard-objects/event.object-metadata';
|
||||
import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository';
|
||||
import { WorkspaceMemberObjectMetadata } from 'src/modules/workspace-member/standard-objects/workspace-member.object-metadata';
|
||||
|
||||
export type SaveEventToDbJobData = {
|
||||
workspaceId: string;
|
||||
recordId: string;
|
||||
userId: string | undefined;
|
||||
objectName: string;
|
||||
operation: string;
|
||||
details: any;
|
||||
@@ -16,39 +20,47 @@ export type SaveEventToDbJobData = {
|
||||
@Injectable()
|
||||
export class SaveEventToDbJob implements MessageQueueJob<SaveEventToDbJobData> {
|
||||
constructor(
|
||||
private readonly dataSourceService: DataSourceService,
|
||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||
@InjectObjectMetadataRepository(WorkspaceMemberObjectMetadata)
|
||||
private readonly workspaceMemberService: WorkspaceMemberRepository,
|
||||
@InjectObjectMetadataRepository(EventObjectMetadata)
|
||||
private readonly eventService: EventRepository,
|
||||
) {}
|
||||
|
||||
// TODO: need to support objects others than "person", "company", "opportunity"
|
||||
async handle(data: SaveEventToDbJobData): Promise<void> {
|
||||
const dataSourceMetadata =
|
||||
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
||||
data.workspaceId,
|
||||
);
|
||||
const workspaceDataSource =
|
||||
await this.workspaceDataSourceService.connectToWorkspaceDataSource(
|
||||
let workspaceMemberId: string | null = null;
|
||||
|
||||
if (data.userId) {
|
||||
const workspaceMember = await this.workspaceMemberService.getByIdOrFail(
|
||||
data.userId,
|
||||
data.workspaceId,
|
||||
);
|
||||
|
||||
const eventType = `${data.operation}.${data.objectName}`;
|
||||
|
||||
// TODO: add "workspaceMember" (who performed the action, need to send it in the event)
|
||||
// TODO: need to support objects others than "person", "company", "opportunities"
|
||||
workspaceMemberId = workspaceMember.id;
|
||||
}
|
||||
|
||||
if (
|
||||
data.objectName != 'person' &&
|
||||
data.objectName != 'company' &&
|
||||
data.objectName != 'opportunities'
|
||||
data.objectName != 'opportunity'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
await workspaceDataSource?.query(
|
||||
`INSERT INTO ${dataSourceMetadata.schema}."event"
|
||||
("name", "properties", "${data.objectName}Id")
|
||||
VALUES ('${eventType}', '${JSON.stringify(data.details)}', '${
|
||||
data.recordId
|
||||
}') RETURNING *`,
|
||||
if (data.details.diff) {
|
||||
// we remove "before" and "after" property for a cleaner/slimmer event payload
|
||||
data.details = {
|
||||
diff: data.details.diff,
|
||||
};
|
||||
}
|
||||
|
||||
await this.eventService.insert(
|
||||
`${data.operation}.${data.objectName}`,
|
||||
data.details,
|
||||
workspaceMemberId,
|
||||
data.objectName,
|
||||
data.recordId,
|
||||
data.workspaceId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
FeatureFlagEntity,
|
||||
FeatureFlagKeys,
|
||||
} from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { objectRecordChangedValues } from 'src/engine/integrations/event-emitter/utils/object-record-changed-values';
|
||||
import { ObjectRecordUpdateEvent } from 'src/engine/integrations/event-emitter/types/object-record-update.event';
|
||||
|
||||
@Injectable()
|
||||
export class EntityEventsToDbListener {
|
||||
@@ -31,7 +33,12 @@ export class EntityEventsToDbListener {
|
||||
}
|
||||
|
||||
@OnEvent('*.updated')
|
||||
async handleUpdate(payload: ObjectRecordCreateEvent<any>) {
|
||||
async handleUpdate(payload: ObjectRecordUpdateEvent<any>) {
|
||||
payload.details.diff = objectRecordChangedValues(
|
||||
payload.details.before,
|
||||
payload.details.after,
|
||||
);
|
||||
|
||||
return this.handle(payload, 'updated');
|
||||
}
|
||||
|
||||
@@ -58,6 +65,7 @@ export class EntityEventsToDbListener {
|
||||
|
||||
this.messageQueueService.add<SaveEventToDbJobData>(SaveEventToDbJob.name, {
|
||||
workspaceId: payload.workspaceId,
|
||||
userId: payload.userId,
|
||||
recordId: payload.recordId,
|
||||
objectName: payload.objectMetadata.nameSingular,
|
||||
operation: operation,
|
||||
|
||||
@@ -9,6 +9,9 @@ import { RecordPositionListener } from 'src/engine/api/graphql/workspace-query-r
|
||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { EventObjectMetadata } from 'src/modules/event/standard-objects/event.object-metadata';
|
||||
import { WorkspaceMemberObjectMetadata } from 'src/modules/workspace-member/standard-objects/workspace-member.object-metadata';
|
||||
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
||||
|
||||
import { WorkspaceQueryRunnerService } from './workspace-query-runner.service';
|
||||
|
||||
@@ -21,6 +24,10 @@ import { EntityEventsToDbListener } from './listeners/entity-events-to-db.listen
|
||||
WorkspaceDataSourceModule,
|
||||
WorkspacePreQueryHookModule,
|
||||
TypeOrmModule.forFeature([Workspace, FeatureFlagEntity], 'core'),
|
||||
ObjectMetadataRepositoryModule.forFeature([
|
||||
WorkspaceMemberObjectMetadata,
|
||||
EventObjectMetadata,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
WorkspaceQueryRunnerService,
|
||||
|
||||
@@ -216,7 +216,7 @@ export class WorkspaceQueryRunnerService {
|
||||
args: CreateManyResolverArgs<Record>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<Record[] | undefined> {
|
||||
const { workspaceId, objectMetadataItem } = options;
|
||||
const { workspaceId, userId, objectMetadataItem } = options;
|
||||
const computedArgs = await this.queryRunnerArgsFactory.create(
|
||||
args,
|
||||
options,
|
||||
@@ -246,6 +246,7 @@ export class WorkspaceQueryRunnerService {
|
||||
parsedResults.forEach((record) => {
|
||||
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.created`, {
|
||||
workspaceId,
|
||||
userId,
|
||||
recordId: record.id,
|
||||
objectMetadata: objectMetadataItem,
|
||||
details: {
|
||||
@@ -270,7 +271,7 @@ export class WorkspaceQueryRunnerService {
|
||||
args: UpdateOneResolverArgs<Record>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<Record | undefined> {
|
||||
const { workspaceId, objectMetadataItem } = options;
|
||||
const { workspaceId, userId, objectMetadataItem } = options;
|
||||
|
||||
const existingRecord = await this.findOne(
|
||||
{ filter: { id: { eq: args.id } } } as FindOneResolverArgs,
|
||||
@@ -300,6 +301,7 @@ export class WorkspaceQueryRunnerService {
|
||||
|
||||
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.updated`, {
|
||||
workspaceId,
|
||||
userId,
|
||||
recordId: (existingRecord as Record).id,
|
||||
objectMetadata: objectMetadataItem,
|
||||
details: {
|
||||
@@ -356,7 +358,7 @@ export class WorkspaceQueryRunnerService {
|
||||
args: DeleteManyResolverArgs<Filter>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<Record[] | undefined> {
|
||||
const { workspaceId, objectMetadataItem } = options;
|
||||
const { workspaceId, userId, objectMetadataItem } = options;
|
||||
const maximumRecordAffected = this.environmentService.get(
|
||||
'MUTATION_MAXIMUM_RECORD_AFFECTED',
|
||||
);
|
||||
@@ -384,6 +386,7 @@ export class WorkspaceQueryRunnerService {
|
||||
parsedResults.forEach((record) => {
|
||||
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.deleted`, {
|
||||
workspaceId,
|
||||
userId,
|
||||
recordId: record.id,
|
||||
objectMetadata: objectMetadataItem,
|
||||
details: {
|
||||
@@ -399,7 +402,7 @@ export class WorkspaceQueryRunnerService {
|
||||
args: DeleteOneResolverArgs,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<Record | undefined> {
|
||||
const { workspaceId, objectMetadataItem } = options;
|
||||
const { workspaceId, userId, objectMetadataItem } = options;
|
||||
const query = await this.workspaceQueryBuilderFactory.deleteOne(
|
||||
args,
|
||||
options,
|
||||
@@ -422,6 +425,7 @@ export class WorkspaceQueryRunnerService {
|
||||
|
||||
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.deleted`, {
|
||||
workspaceId,
|
||||
userId,
|
||||
recordId: args.id,
|
||||
objectMetadata: objectMetadataItem,
|
||||
details: {
|
||||
|
||||
@@ -8,4 +8,4 @@ export * from './string-filter.input-type';
|
||||
export * from './time-filter.input-type';
|
||||
export * from './uuid-filter.input-type';
|
||||
export * from './boolean-filter.input-type';
|
||||
export * from './json-filter.input-type';
|
||||
export * from './raw-json-filter.input-type';
|
||||
|
||||
@@ -2,8 +2,8 @@ import { GraphQLInputObjectType } from 'graphql';
|
||||
|
||||
import { FilterIs } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/filter-is.input-type';
|
||||
|
||||
export const JsonFilterType = new GraphQLInputObjectType({
|
||||
name: 'JsonFilter',
|
||||
export const RawJsonFilterType = new GraphQLInputObjectType({
|
||||
name: 'RawJsonFilter',
|
||||
fields: {
|
||||
is: { type: FilterIs },
|
||||
},
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
IntFilterType,
|
||||
BooleanFilterType,
|
||||
BigFloatFilterType,
|
||||
JsonFilterType,
|
||||
RawJsonFilterType,
|
||||
} from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input';
|
||||
import { OrderByDirectionType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/enum';
|
||||
import { BigFloatScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
@@ -70,7 +70,7 @@ export class TypeMapperService {
|
||||
[FieldMetadataType.PROBABILITY, GraphQLFloat],
|
||||
[FieldMetadataType.RELATION, GraphQLID],
|
||||
[FieldMetadataType.POSITION, PositionScalarType],
|
||||
[FieldMetadataType.JSON, GraphQLJSON],
|
||||
[FieldMetadataType.RAW_JSON, GraphQLJSON],
|
||||
]);
|
||||
|
||||
return typeScalarMapping.get(fieldMetadataType);
|
||||
@@ -102,7 +102,7 @@ export class TypeMapperService {
|
||||
[FieldMetadataType.PROBABILITY, FloatFilterType],
|
||||
[FieldMetadataType.RELATION, UUIDFilterType],
|
||||
[FieldMetadataType.POSITION, FloatFilterType],
|
||||
[FieldMetadataType.JSON, JsonFilterType],
|
||||
[FieldMetadataType.RAW_JSON, RawJsonFilterType],
|
||||
]);
|
||||
|
||||
return typeFilterMapping.get(fieldMetadataType);
|
||||
@@ -126,7 +126,7 @@ export class TypeMapperService {
|
||||
[FieldMetadataType.SELECT, OrderByDirectionType],
|
||||
[FieldMetadataType.MULTI_SELECT, OrderByDirectionType],
|
||||
[FieldMetadataType.POSITION, OrderByDirectionType],
|
||||
[FieldMetadataType.JSON, OrderByDirectionType],
|
||||
[FieldMetadataType.RAW_JSON, OrderByDirectionType],
|
||||
]);
|
||||
|
||||
return typeOrderByMapping.get(fieldMetadataType);
|
||||
|
||||
@@ -69,7 +69,7 @@ const getSchemaComponentsProperties = (
|
||||
),
|
||||
};
|
||||
break;
|
||||
case FieldMetadataType.JSON:
|
||||
case FieldMetadataType.RAW_JSON:
|
||||
type: 'object';
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -4,5 +4,6 @@ export class ObjectRecordUpdateEvent<T> extends ObjectRecordBaseEvent {
|
||||
details: {
|
||||
before: T;
|
||||
after: T;
|
||||
diff?: Partial<T>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metad
|
||||
export class ObjectRecordBaseEvent {
|
||||
workspaceId: string;
|
||||
recordId: string;
|
||||
userId?: string;
|
||||
objectMetadata: ObjectMetadataInterface;
|
||||
details: any;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
@@ -17,7 +17,7 @@ export class FieldMetadataDefaultValueString {
|
||||
value: string | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValueJson {
|
||||
export class FieldMetadataDefaultValueRawJson {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsJSON()
|
||||
value: JSON | null;
|
||||
|
||||
@@ -36,7 +36,7 @@ export enum FieldMetadataType {
|
||||
MULTI_SELECT = 'MULTI_SELECT',
|
||||
RELATION = 'RELATION',
|
||||
POSITION = 'POSITION',
|
||||
JSON = 'JSON',
|
||||
RAW_JSON = 'RAW_JSON',
|
||||
}
|
||||
|
||||
@Entity('fieldMetadata')
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
FieldMetadataDefaultValueCurrency,
|
||||
FieldMetadataDefaultValueDateTime,
|
||||
FieldMetadataDefaultValueFullName,
|
||||
FieldMetadataDefaultValueJson,
|
||||
FieldMetadataDefaultValueRawJson,
|
||||
FieldMetadataDefaultValueLink,
|
||||
FieldMetadataDefaultValueNumber,
|
||||
FieldMetadataDefaultValueString,
|
||||
@@ -51,7 +51,7 @@ type FieldMetadataDefaultValueMapping = {
|
||||
[FieldMetadataType.RATING]: FieldMetadataDefaultValueString;
|
||||
[FieldMetadataType.SELECT]: FieldMetadataDefaultValueString;
|
||||
[FieldMetadataType.MULTI_SELECT]: FieldMetadataDefaultValueStringArray;
|
||||
[FieldMetadataType.JSON]: FieldMetadataDefaultValueJson;
|
||||
[FieldMetadataType.RAW_JSON]: FieldMetadataDefaultValueRawJson;
|
||||
};
|
||||
|
||||
type DefaultValueByFieldMetadata<T extends FieldMetadataType | 'default'> = [
|
||||
|
||||
@@ -35,7 +35,7 @@ export function generateTargetColumnMap(
|
||||
case FieldMetadataType.SELECT:
|
||||
case FieldMetadataType.MULTI_SELECT:
|
||||
case FieldMetadataType.POSITION:
|
||||
case FieldMetadataType.JSON:
|
||||
case FieldMetadataType.RAW_JSON:
|
||||
return {
|
||||
value: columnName,
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
FieldMetadataDefaultValueCurrency,
|
||||
FieldMetadataDefaultValueDateTime,
|
||||
FieldMetadataDefaultValueFullName,
|
||||
FieldMetadataDefaultValueJson,
|
||||
FieldMetadataDefaultValueRawJson,
|
||||
FieldMetadataDefaultValueLink,
|
||||
FieldMetadataDefaultValueNumber,
|
||||
FieldMetadataDefaultValueString,
|
||||
@@ -40,7 +40,7 @@ export const defaultValueValidatorsMap = {
|
||||
[FieldMetadataType.RATING]: [FieldMetadataDefaultValueString],
|
||||
[FieldMetadataType.SELECT]: [FieldMetadataDefaultValueString],
|
||||
[FieldMetadataType.MULTI_SELECT]: [FieldMetadataDefaultValueStringArray],
|
||||
[FieldMetadataType.JSON]: [FieldMetadataDefaultValueJson],
|
||||
[FieldMetadataType.RAW_JSON]: [FieldMetadataDefaultValueRawJson],
|
||||
};
|
||||
|
||||
export const validateDefaultValueForType = (
|
||||
|
||||
@@ -29,7 +29,7 @@ export const fieldMetadataTypeToColumnType = <Type extends FieldMetadataType>(
|
||||
case FieldMetadataType.SELECT:
|
||||
case FieldMetadataType.MULTI_SELECT:
|
||||
return 'enum';
|
||||
case FieldMetadataType.JSON:
|
||||
case FieldMetadataType.RAW_JSON:
|
||||
return 'jsonb';
|
||||
default:
|
||||
throw new Error(`Cannot convert ${fieldMetadataType} to column type.`);
|
||||
|
||||
@@ -67,7 +67,7 @@ export class WorkspaceMigrationFactory {
|
||||
[FieldMetadataType.NUMERIC, { factory: this.basicColumnActionFactory }],
|
||||
[FieldMetadataType.NUMBER, { factory: this.basicColumnActionFactory }],
|
||||
[FieldMetadataType.POSITION, { factory: this.basicColumnActionFactory }],
|
||||
[FieldMetadataType.JSON, { factory: this.basicColumnActionFactory }],
|
||||
[FieldMetadataType.RAW_JSON, { factory: this.basicColumnActionFactory }],
|
||||
[
|
||||
FieldMetadataType.PROBABILITY,
|
||||
{ factory: this.basicColumnActionFactory },
|
||||
|
||||
@@ -5,6 +5,7 @@ import { CalendarEventRepository } from 'src/modules/calendar/repositories/calen
|
||||
import { CompanyRepository } from 'src/modules/company/repositories/company.repository';
|
||||
import { BlocklistRepository } from 'src/modules/connected-account/repositories/blocklist.repository';
|
||||
import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository';
|
||||
import { EventRepository } from 'src/modules/event/repositiories/event.repository';
|
||||
import { MessageChannelMessageAssociationRepository } from 'src/modules/messaging/repositories/message-channel-message-association.repository';
|
||||
import { MessageChannelRepository } from 'src/modules/messaging/repositories/message-channel.repository';
|
||||
import { MessageParticipantRepository } from 'src/modules/messaging/repositories/message-participant.repository';
|
||||
@@ -22,6 +23,7 @@ export const metadataToRepositoryMapping = {
|
||||
CalendarEventObjectMetadata: CalendarEventRepository,
|
||||
CompanyObjectMetadata: CompanyRepository,
|
||||
ConnectedAccountObjectMetadata: ConnectedAccountRepository,
|
||||
EventObjectMetadata: EventRepository,
|
||||
MessageChannelMessageAssociationObjectMetadata:
|
||||
MessageChannelMessageAssociationRepository,
|
||||
MessageChannelObjectMetadata: MessageChannelRepository,
|
||||
|
||||
@@ -22,7 +22,7 @@ export const mapFieldMetadataTypeToDataType = (
|
||||
return 'boolean';
|
||||
case FieldMetadataType.DATE_TIME:
|
||||
return 'timestamp';
|
||||
case FieldMetadataType.JSON:
|
||||
case FieldMetadataType.RAW_JSON:
|
||||
return 'jsonb';
|
||||
case FieldMetadataType.RATING:
|
||||
case FieldMetadataType.SELECT:
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ export class EventObjectMetadata extends BaseObjectMetadata {
|
||||
|
||||
@FieldMetadata({
|
||||
standardId: eventStandardFieldIds.properties,
|
||||
type: FieldMetadataType.JSON,
|
||||
type: FieldMetadataType.RAW_JSON,
|
||||
label: 'Event details',
|
||||
description: 'Json value for event details',
|
||||
icon: 'IconListDetails',
|
||||
|
||||
Reference in New Issue
Block a user