mirror of
https://github.com/lingble/twenty.git
synced 2025-11-02 05:37:56 +00:00
Add JSON field type and Event object (#4566)
* Add JSON field type and Event object * Simplify code * Adress PR comments and add featureFlag
This commit is contained in:
@@ -66,10 +66,32 @@ export type AuthTokens = {
|
||||
export type Billing = {
|
||||
__typename?: 'Billing';
|
||||
billingFreeTrialDurationInDays?: Maybe<Scalars['Float']['output']>;
|
||||
billingUrl: Scalars['String']['output'];
|
||||
billingUrl?: Maybe<Scalars['String']['output']>;
|
||||
isBillingEnabled: Scalars['Boolean']['output'];
|
||||
};
|
||||
|
||||
export type BillingSubscription = {
|
||||
__typename?: 'BillingSubscription';
|
||||
id: Scalars['ID']['output'];
|
||||
status: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type BillingSubscriptionFilter = {
|
||||
and?: InputMaybe<Array<BillingSubscriptionFilter>>;
|
||||
id?: InputMaybe<IdFilterComparison>;
|
||||
or?: InputMaybe<Array<BillingSubscriptionFilter>>;
|
||||
};
|
||||
|
||||
export type BillingSubscriptionSort = {
|
||||
direction: SortDirection;
|
||||
field: BillingSubscriptionSortFields;
|
||||
nulls?: InputMaybe<SortNulls>;
|
||||
};
|
||||
|
||||
export enum BillingSubscriptionSortFields {
|
||||
Id = 'id'
|
||||
}
|
||||
|
||||
export type BooleanFieldComparison = {
|
||||
is?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
isNot?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
@@ -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<Scalars['DateTime']['output']>;
|
||||
@@ -831,7 +871,9 @@ export type Workspace = {
|
||||
__typename?: 'Workspace';
|
||||
activationStatus: Scalars['String']['output'];
|
||||
allowImpersonation: Scalars['Boolean']['output'];
|
||||
billingSubscriptions?: Maybe<Array<BillingSubscription>>;
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
currentBillingSubscription?: Maybe<BillingSubscription>;
|
||||
deletedAt?: Maybe<Scalars['DateTime']['output']>;
|
||||
displayName?: Maybe<Scalars['String']['output']>;
|
||||
domainName?: Maybe<Scalars['String']['output']>;
|
||||
@@ -844,6 +886,12 @@ export type Workspace = {
|
||||
};
|
||||
|
||||
|
||||
export type WorkspaceBillingSubscriptionsArgs = {
|
||||
filter?: BillingSubscriptionFilter;
|
||||
sorting?: Array<BillingSubscriptionSort>;
|
||||
};
|
||||
|
||||
|
||||
export type WorkspaceFeatureFlagsArgs = {
|
||||
filter?: FeatureFlagFilter;
|
||||
sorting?: Array<FeatureFlagSort>;
|
||||
@@ -886,6 +934,7 @@ export type Field = {
|
||||
label: Scalars['String']['output'];
|
||||
name: Scalars['String']['output'];
|
||||
options?: Maybe<Scalars['JSON']['output']>;
|
||||
relationDefinition?: Maybe<RelationDefinition>;
|
||||
toRelationMetadata?: Maybe<Relation>;
|
||||
type: FieldMetadataType;
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
|
||||
@@ -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<typeof useGetTimelineThreadsFromPersonIdQuery>;
|
||||
export type GetTimelineThreadsFromPersonIdLazyQueryHookResult = ReturnType<typeof useGetTimelineThreadsFromPersonIdLazyQuery>;
|
||||
export type GetTimelineThreadsFromPersonIdQueryResult = Apollo.QueryResult<GetTimelineThreadsFromPersonIdQuery, GetTimelineThreadsFromPersonIdQueryVariables>;
|
||||
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<CreateEventMutation, CreateEventMutationVariables>;
|
||||
export type TrackMutationFn = Apollo.MutationFunction<TrackMutation, TrackMutationVariables>;
|
||||
|
||||
/**
|
||||
* __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<CreateEventMutation, CreateEventMutationVariables>) {
|
||||
export function useTrackMutation(baseOptions?: Apollo.MutationHookOptions<TrackMutation, TrackMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<CreateEventMutation, CreateEventMutationVariables>(CreateEventDocument, options);
|
||||
return Apollo.useMutation<TrackMutation, TrackMutationVariables>(TrackDocument, options);
|
||||
}
|
||||
export type CreateEventMutationHookResult = ReturnType<typeof useCreateEventMutation>;
|
||||
export type CreateEventMutationResult = Apollo.MutationResult<CreateEventMutation>;
|
||||
export type CreateEventMutationOptions = Apollo.BaseMutationOptions<CreateEventMutation, CreateEventMutationVariables>;
|
||||
export type TrackMutationHookResult = ReturnType<typeof useTrackMutation>;
|
||||
export type TrackMutationResult = Apollo.MutationResult<TrackMutation>;
|
||||
export type TrackMutationOptions = Apollo.BaseMutationOptions<TrackMutation, TrackMutationVariables>;
|
||||
export const ChallengeDocument = gql`
|
||||
mutation Challenge($email: String!, $password: String!) {
|
||||
challenge(email: $email, password: $password) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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' },
|
||||
},
|
||||
});
|
||||
}),
|
||||
|
||||
@@ -25,6 +25,11 @@ export const seedFeatureFlags = async (
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKeys.IsEventObjectEnabled,
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
])
|
||||
.execute();
|
||||
};
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -36,6 +36,7 @@ export enum FieldMetadataType {
|
||||
MULTI_SELECT = 'MULTI_SELECT',
|
||||
RELATION = 'RELATION',
|
||||
POSITION = 'POSITION',
|
||||
JSON = 'JSON',
|
||||
}
|
||||
|
||||
@Entity('fieldMetadata')
|
||||
|
||||
@@ -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<T extends FieldMetadataType | 'default'> = [
|
||||
|
||||
@@ -35,6 +35,7 @@ export function generateTargetColumnMap(
|
||||
case FieldMetadataType.SELECT:
|
||||
case FieldMetadataType.MULTI_SELECT:
|
||||
case FieldMetadataType.POSITION:
|
||||
case FieldMetadataType.JSON:
|
||||
return {
|
||||
value: columnName,
|
||||
};
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -29,6 +29,8 @@ export const fieldMetadataTypeToColumnType = <Type extends FieldMetadataType>(
|
||||
case FieldMetadataType.SELECT:
|
||||
case FieldMetadataType.MULTI_SELECT:
|
||||
return 'enum';
|
||||
case FieldMetadataType.JSON:
|
||||
return 'jsonb';
|
||||
default:
|
||||
throw new Error(`Cannot convert ${fieldMetadataType} to column type.`);
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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<SaveEventToDbJobData> {
|
||||
constructor(
|
||||
private readonly dataSourceService: DataSourceService,
|
||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||
) {}
|
||||
|
||||
async handle(data: SaveEventToDbJobData): Promise<void> {
|
||||
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 *`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<FeatureFlagEntity>,
|
||||
) {}
|
||||
|
||||
@OnEvent('*.created')
|
||||
async handleCreate(payload: ObjectRecordCreateEvent<any>) {
|
||||
return this.handle(payload, 'created');
|
||||
}
|
||||
|
||||
@OnEvent('*.updated')
|
||||
async handleUpdate(payload: ObjectRecordCreateEvent<any>) {
|
||||
return this.handle(payload, 'updated');
|
||||
}
|
||||
|
||||
// @OnEvent('*.deleted') - TODO: implement when we have soft deleted
|
||||
// ....
|
||||
|
||||
private async handle(
|
||||
payload: ObjectRecordCreateEvent<any>,
|
||||
operation: string,
|
||||
) {
|
||||
const isEventObjectEnabledFeatureFlag =
|
||||
await this.featureFlagRepository.findOneBy({
|
||||
workspaceId: payload.workspaceId,
|
||||
key: FeatureFlagKeys.IsEventObjectEnabled,
|
||||
value: true,
|
||||
});
|
||||
|
||||
if (
|
||||
!isEventObjectEnabledFeatureFlag ||
|
||||
!isEventObjectEnabledFeatureFlag.value
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.messageQueueService.add<SaveEventToDbJobData>(SaveEventToDbJob.name, {
|
||||
workspaceId: payload.workspaceId,
|
||||
recordId: payload.recordId,
|
||||
objectName: payload.objectMetadata.nameSingular,
|
||||
operation: operation,
|
||||
details: payload.details,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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<any>) {
|
||||
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 ||
|
||||
|
||||
@@ -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],
|
||||
})
|
||||
|
||||
@@ -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<any>);
|
||||
});
|
||||
@@ -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<any>);
|
||||
|
||||
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<any>);
|
||||
});
|
||||
|
||||
@@ -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<any>);
|
||||
|
||||
return parsedResults?.[0];
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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<T extends BaseObjectMetadata> {
|
||||
workspaceId: string;
|
||||
createdRecord: T;
|
||||
createdObjectMetadata: CreatedObjectMetadata;
|
||||
export class ObjectRecordCreateEvent<T> extends ObjectRecordBaseEvent {
|
||||
details: {
|
||||
after: T;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<T extends BaseObjectMetadata> {
|
||||
workspaceId: string;
|
||||
deletedRecord: T;
|
||||
export class ObjectRecordDeleteEvent<T> extends ObjectRecordBaseEvent {
|
||||
details: {
|
||||
before: T;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<T extends BaseObjectMetadata> {
|
||||
workspaceId: string;
|
||||
previousRecord: T;
|
||||
updatedRecord: T;
|
||||
export class ObjectRecordUpdateEvent<T> extends ObjectRecordBaseEvent {
|
||||
details: {
|
||||
before: T;
|
||||
after: T;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -9,4 +9,5 @@ export enum MessageQueue {
|
||||
calendarQueue = 'calendar-queue',
|
||||
billingQueue = 'billing-queue',
|
||||
recordPositionBackfillQueue = 'record-position-backfill-queue',
|
||||
entityEventsToDbQueue = 'entity-events-to-db-queue',
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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' })
|
||||
|
||||
@@ -69,6 +69,9 @@ const getSchemaComponentsProperties = (
|
||||
),
|
||||
};
|
||||
break;
|
||||
case FieldMetadataType.JSON:
|
||||
type: 'object';
|
||||
break;
|
||||
default:
|
||||
itemProperty.type = 'string';
|
||||
break;
|
||||
|
||||
@@ -62,8 +62,10 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
|
||||
new ObjectRecordCreateEvent<WorkspaceMemberObjectMetadata>();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<CreateCompaniesAndContactsAfterSyncJobData>(
|
||||
CreateCompaniesAndContactsAfterSyncJob.name,
|
||||
{
|
||||
workspaceId: payload.workspaceId,
|
||||
messageChannelId: payload.updatedRecord.id,
|
||||
messageChannelId: payload.recordId,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export class MessagingPersonListener {
|
||||
async handleCreatedEvent(
|
||||
payload: ObjectRecordCreateEvent<PersonObjectMetadata>,
|
||||
) {
|
||||
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<MatchMessageParticipantsJobData>(
|
||||
MatchMessageParticipantJob.name,
|
||||
{
|
||||
workspaceId: payload.workspaceId,
|
||||
email: payload.updatedRecord.email,
|
||||
personId: payload.updatedRecord.id,
|
||||
email: payload.details.after.email,
|
||||
personId: payload.recordId,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export class MessagingWorkspaceMemberListener {
|
||||
async handleCreatedEvent(
|
||||
payload: ObjectRecordCreateEvent<WorkspaceMemberObjectMetadata>,
|
||||
) {
|
||||
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<MatchMessageParticipantsJobData>(
|
||||
MatchMessageParticipantJob.name,
|
||||
{
|
||||
workspaceId: payload.workspaceId,
|
||||
email: payload.updatedRecord.userEmail,
|
||||
workspaceMemberId: payload.updatedRecord.id,
|
||||
email: payload.details.after.userEmail,
|
||||
workspaceMemberId: payload.recordId,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user