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:
Félix Malfait
2024-03-19 21:54:08 +01:00
committed by GitHub
parent 4ab426c52a
commit 4bfb90657f
51 changed files with 575 additions and 117 deletions

View File

@@ -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'];

View File

@@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) => {

View File

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

View File

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

View File

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

View File

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

View File

@@ -25,6 +25,11 @@ export const seedFeatureFlags = async (
workspaceId: workspaceId,
value: true,
},
{
key: FeatureFlagKeys.IsEventObjectEnabled,
workspaceId: workspaceId,
value: true,
},
])
.execute();
};

View File

@@ -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()

View File

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

View File

@@ -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'> = [

View File

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

View File

@@ -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 = (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 {

View File

@@ -9,4 +9,5 @@ export enum MessageQueue {
calendarQueue = 'calendar-queue',
billingQueue = 'billing-queue',
recordPositionBackfillQueue = 'record-position-backfill-queue',
entityEventsToDbQueue = 'entity-events-to-db-queue',
}

View File

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

View File

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

View File

@@ -69,6 +69,9 @@ const getSchemaComponentsProperties = (
),
};
break;
case FieldMetadataType.JSON:
type: 'object';
break;
default:
itemProperty.type = 'string';
break;

View File

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

View File

@@ -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:

View File

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

View File

@@ -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 = {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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