Skip to content

Commit 7d2394b

Browse files
committed
Add relations in database event trigger output data (#11820)
## Done - add relations in dropdown variables - add relations in worklfow run inputs - use objectMetadataMaps in workflow folder ## To do - does not work with rest api calls, will be fixed after #11349 is merged - waiting for crud action relation fields twentyhq/core-team-issues#509
1 parent 01b40e1 commit 7d2394b

File tree

36 files changed

+684
-333
lines changed

36 files changed

+684
-333
lines changed

packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { FormRichTextV2FieldInput } from '@/object-record/record-field/form-type
1414
import { FormSelectFieldInput } from '@/object-record/record-field/form-types/components/FormSelectFieldInput';
1515
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
1616
import { FormUuidFieldInput } from '@/object-record/record-field/form-types/components/FormUuidFieldInput';
17+
import { FormRelationToOneFieldInput } from '@/object-record/record-field/form-types/components/FormRelationToOneFieldInput';
1718
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
1819
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
1920
import {
@@ -24,6 +25,8 @@ import {
2425
FieldMetadata,
2526
FieldMultiSelectValue,
2627
FieldPhonesValue,
28+
FieldRelationToOneValue,
29+
FieldRelationValue,
2730
FieldRichTextV2Value,
2831
FormFieldCurrencyValue,
2932
} from '@/object-record/record-field/types/FieldMetadata';
@@ -43,6 +46,7 @@ import { isFieldRichTextV2 } from '@/object-record/record-field/types/guards/isF
4346
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
4447
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
4548
import { isFieldUuid } from '@/object-record/record-field/types/guards/isFieldUuid';
49+
import { isFieldRelationToOneObject } from '@/object-record/record-field/types/guards/isFieldRelationToOneObject';
4650
import { JsonValue } from 'type-fest';
4751

4852
type FormFieldInputProps = {
@@ -205,5 +209,15 @@ export const FormFieldInput = ({
205209
readonly={readonly}
206210
placeholder={placeholder}
207211
/>
212+
) : isFieldRelationToOneObject(field) ? (
213+
<FormRelationToOneFieldInput
214+
label={field.label}
215+
objectNameSingular={field.metadata.relationObjectMetadataNameSingular}
216+
defaultValue={defaultValue as FieldRelationValue<FieldRelationToOneValue>}
217+
onChange={onChange}
218+
VariablePicker={VariablePicker}
219+
readonly={readonly}
220+
placeholder={placeholder}
221+
/>
208222
) : null;
209223
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
2+
import { FormSingleRecordPicker } from '@/object-record/record-field/form-types/components/FormSingleRecordPicker';
3+
import { isDefined } from 'twenty-shared/utils';
4+
import { JsonValue } from 'type-fest';
5+
import {
6+
FieldRelationToOneValue,
7+
FieldRelationValue,
8+
} from '@/object-record/record-field/types/FieldMetadata';
9+
10+
export type FormRelationToOneFieldInputProps = {
11+
label?: string;
12+
objectNameSingular?: string;
13+
defaultValue: FieldRelationValue<FieldRelationToOneValue>;
14+
onChange: (value: JsonValue) => void;
15+
readonly?: boolean;
16+
VariablePicker?: VariablePickerComponent;
17+
placeholder?: string;
18+
};
19+
20+
export const FormRelationToOneFieldInput = ({
21+
label,
22+
objectNameSingular,
23+
onChange,
24+
defaultValue,
25+
readonly,
26+
VariablePicker,
27+
}: FormRelationToOneFieldInputProps) => {
28+
return (
29+
isDefined(objectNameSingular) && (
30+
<FormSingleRecordPicker
31+
label={label}
32+
defaultValue={defaultValue?.id}
33+
onChange={(recordId) => {
34+
onChange({
35+
id: recordId,
36+
});
37+
}}
38+
objectNameSingular={objectNameSingular}
39+
disabled={readonly}
40+
VariablePicker={VariablePicker}
41+
/>
42+
)
43+
);
44+
};

packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormSingleRecordPicker.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ type FormSingleRecordPickerValue =
5858

5959
export type FormSingleRecordPickerProps = {
6060
label?: string;
61-
defaultValue: RecordId | Variable;
61+
defaultValue?: RecordId | Variable;
6262
onChange: (value: RecordId | Variable) => void;
6363
objectNameSingular: string;
6464
disabled?: boolean;
@@ -95,7 +95,7 @@ export const FormSingleRecordPicker = ({
9595
: '',
9696
objectNameSingular,
9797
withSoftDeleted: true,
98-
skip: !isValidUuid(defaultValue),
98+
skip: !isDefined(defaultValue) || !isValidUuid(defaultValue),
9999
});
100100

101101
const dropdownId = `form-record-picker-${objectNameSingular}`;

packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { HorizontalSeparator, useIcons } from 'twenty-ui/display';
1717
import { SelectOption } from 'twenty-ui/input';
1818
import { JsonValue } from 'type-fest';
1919
import { useDebouncedCallback } from 'use-debounce';
20-
import { FieldMetadataType } from '~/generated/graphql';
20+
import { shouldDisplayFormField } from '@/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField';
2121

2222
type WorkflowEditActionCreateRecordProps = {
2323
action: WorkflowCreateRecordAction;
@@ -92,11 +92,8 @@ export const WorkflowEditActionCreateRecord = ({
9292
const viewFields = indexView?.viewFields ?? [];
9393

9494
const inlineFieldMetadataItems = objectMetadataItem?.fields
95-
.filter(
96-
(fieldMetadataItem) =>
97-
fieldMetadataItem.type !== FieldMetadataType.RELATION &&
98-
!fieldMetadataItem.isSystem &&
99-
fieldMetadataItem.isActive,
95+
.filter((fieldMetadataItem) =>
96+
shouldDisplayFormField({ fieldMetadataItem, actionType: action.type }),
10097
)
10198
.map((fieldMetadataItem) => {
10299
const viewField = viewFields.find(

packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord.tsx

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { HorizontalSeparator, useIcons } from 'twenty-ui/display';
1818
import { SelectOption } from 'twenty-ui/input';
1919
import { JsonValue } from 'type-fest';
2020
import { useDebouncedCallback } from 'use-debounce';
21-
import { FieldMetadataType } from '~/generated-metadata/graphql';
21+
import { shouldDisplayFormField } from '@/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField';
2222

2323
type WorkflowEditActionUpdateRecordProps = {
2424
action: WorkflowUpdateRecordAction;
@@ -39,24 +39,6 @@ type UpdateRecordFormData = {
3939
[field: string]: unknown;
4040
};
4141

42-
const AVAILABLE_FIELD_METADATA_TYPES = [
43-
FieldMetadataType.TEXT,
44-
FieldMetadataType.NUMBER,
45-
FieldMetadataType.DATE,
46-
FieldMetadataType.BOOLEAN,
47-
FieldMetadataType.SELECT,
48-
FieldMetadataType.MULTI_SELECT,
49-
FieldMetadataType.EMAILS,
50-
FieldMetadataType.LINKS,
51-
FieldMetadataType.FULL_NAME,
52-
FieldMetadataType.ADDRESS,
53-
FieldMetadataType.PHONES,
54-
FieldMetadataType.CURRENCY,
55-
FieldMetadataType.DATE_TIME,
56-
FieldMetadataType.RAW_JSON,
57-
FieldMetadataType.UUID,
58-
];
59-
6042
export const WorkflowEditActionUpdateRecord = ({
6143
action,
6244
actionOptions,
@@ -102,11 +84,8 @@ export const WorkflowEditActionUpdateRecord = ({
10284
const objectNameSingular = selectedObjectMetadataItem?.nameSingular;
10385

10486
const inlineFieldMetadataItems = selectedObjectMetadataItem?.fields
105-
.filter(
106-
(fieldMetadataItem) =>
107-
!fieldMetadataItem.isSystem &&
108-
fieldMetadataItem.isActive &&
109-
AVAILABLE_FIELD_METADATA_TYPES.includes(fieldMetadataItem.type),
87+
.filter((fieldMetadataItem) =>
88+
shouldDisplayFormField({ fieldMetadataItem, actionType: action.type }),
11089
)
11190
.sort((fieldMetadataItemA, fieldMetadataItemB) =>
11291
fieldMetadataItemA.name.localeCompare(fieldMetadataItemB.name),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
2+
import { WorkflowActionType } from '@/workflow/types/Workflow';
3+
import { FieldMetadataType } from '~/generated/graphql';
4+
5+
const DISPLAYABLE_FIELD_TYPES_FOR_UPDATE = [
6+
FieldMetadataType.TEXT,
7+
FieldMetadataType.NUMBER,
8+
FieldMetadataType.DATE,
9+
FieldMetadataType.BOOLEAN,
10+
FieldMetadataType.SELECT,
11+
FieldMetadataType.MULTI_SELECT,
12+
FieldMetadataType.EMAILS,
13+
FieldMetadataType.LINKS,
14+
FieldMetadataType.FULL_NAME,
15+
FieldMetadataType.ADDRESS,
16+
FieldMetadataType.PHONES,
17+
FieldMetadataType.CURRENCY,
18+
FieldMetadataType.DATE_TIME,
19+
FieldMetadataType.RAW_JSON,
20+
FieldMetadataType.UUID,
21+
];
22+
23+
export const shouldDisplayFormField = ({
24+
fieldMetadataItem,
25+
actionType,
26+
}: {
27+
fieldMetadataItem: FieldMetadataItem;
28+
actionType: WorkflowActionType;
29+
}) => {
30+
let isTypeAllowedForAction = false;
31+
32+
switch (actionType) {
33+
case 'CREATE_RECORD':
34+
isTypeAllowedForAction =
35+
fieldMetadataItem.type !== FieldMetadataType.RELATION;
36+
break;
37+
case 'UPDATE_RECORD':
38+
isTypeAllowedForAction = DISPLAYABLE_FIELD_TYPES_FOR_UPDATE.includes(
39+
fieldMetadataItem.type,
40+
);
41+
break;
42+
default:
43+
throw new Error(`Action "${actionType}" is not supported`);
44+
}
45+
return (
46+
(isTypeAllowedForAction ||
47+
fieldMetadataItem.settings?.['relationType'] === 'MANY_TO_ONE') &&
48+
!fieldMetadataItem.isSystem &&
49+
fieldMetadataItem.isActive
50+
);
51+
};

packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -321,8 +321,8 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
321321
);
322322

323323
this.apiEventEmitterService.emitUpdateEvents({
324-
existingRecords: [existingRecord],
325-
records: [record],
324+
existingRecords: structuredClone([existingRecord]),
325+
records: structuredClone([record]),
326326
updatedFields: Object.keys(formattedPartialRecordToUpdate),
327327
authContext,
328328
objectMetadataItem: objectMetadataItemWithFieldMaps,
@@ -366,7 +366,7 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
366366
}
367367

368368
this.apiEventEmitterService.emitCreateEvents({
369-
records: formattedInsertedRecords,
369+
records: structuredClone(formattedInsertedRecords),
370370
authContext,
371371
objectMetadataItem: objectMetadataItemWithFieldMaps,
372372
});

packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-one-resolver.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export class GraphqlQueryCreateOneResolverService extends GraphqlQueryBaseResolv
5454
);
5555

5656
this.apiEventEmitterService.emitCreateEvents({
57-
records: upsertedRecords,
57+
records: structuredClone(upsertedRecords),
5858
authContext,
5959
objectMetadataItem: objectMetadataItemWithFieldMaps,
6060
});

packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-many-resolver.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export class GraphqlQueryDeleteManyResolverService extends GraphqlQueryBaseResol
5656
);
5757

5858
this.apiEventEmitterService.emitDeletedEvents({
59-
records: formattedDeletedRecords,
59+
records: structuredClone(formattedDeletedRecords),
6060
authContext,
6161
objectMetadataItem: objectMetadataItemWithFieldMaps,
6262
});

packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-one-resolver.service.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,6 @@ export class GraphqlQueryDeleteOneResolverService extends GraphqlQueryBaseResolv
4848
objectMetadataMaps,
4949
);
5050

51-
this.apiEventEmitterService.emitDeletedEvents({
52-
records: formattedDeletedRecords,
53-
authContext,
54-
objectMetadataItem: objectMetadataItemWithFieldMaps,
55-
});
56-
5751
if (formattedDeletedRecords.length === 0) {
5852
throw new GraphqlQueryRunnerException(
5953
'Record not found',
@@ -63,6 +57,12 @@ export class GraphqlQueryDeleteOneResolverService extends GraphqlQueryBaseResolv
6357

6458
const deletedRecord = formattedDeletedRecords[0];
6559

60+
this.apiEventEmitterService.emitDeletedEvents({
61+
records: structuredClone(formattedDeletedRecords),
62+
authContext,
63+
objectMetadataItem: objectMetadataItemWithFieldMaps,
64+
});
65+
6666
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {
6767
await this.processNestedRelationsHelper.processNestedRelations({
6868
objectMetadataMaps,

packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export class GraphqlQueryDestroyManyResolverService extends GraphqlQueryBaseReso
5454
);
5555

5656
this.apiEventEmitterService.emitDestroyEvents({
57-
records: deletedRecords,
57+
records: structuredClone(deletedRecords),
5858
authContext,
5959
objectMetadataItem: objectMetadataItemWithFieldMaps,
6060
});

packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export class GraphqlQueryDestroyOneResolverService extends GraphqlQueryBaseResol
5454
);
5555

5656
this.apiEventEmitterService.emitDestroyEvents({
57-
records: deletedRecords,
57+
records: structuredClone(deletedRecords),
5858
authContext,
5959
objectMetadataItem: objectMetadataItemWithFieldMaps,
6060
});

packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export class GraphqlQueryRestoreManyResolverService extends GraphqlQueryBaseReso
5656
);
5757

5858
this.apiEventEmitterService.emitRestoreEvents({
59-
records: formattedRestoredRecords,
59+
records: structuredClone(formattedRestoredRecords),
6060
authContext,
6161
objectMetadataItem: objectMetadataItemWithFieldMaps,
6262
});

packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,6 @@ export class GraphqlQueryRestoreOneResolverService extends GraphqlQueryBaseResol
4848
objectMetadataMaps,
4949
);
5050

51-
this.apiEventEmitterService.emitRestoreEvents({
52-
records: formattedRestoredRecords,
53-
authContext,
54-
objectMetadataItem: objectMetadataItemWithFieldMaps,
55-
});
56-
5751
if (formattedRestoredRecords.length === 0) {
5852
throw new GraphqlQueryRunnerException(
5953
'Record not found',
@@ -63,6 +57,12 @@ export class GraphqlQueryRestoreOneResolverService extends GraphqlQueryBaseResol
6357

6458
const restoredRecord = formattedRestoredRecords[0];
6559

60+
this.apiEventEmitterService.emitRestoreEvents({
61+
records: structuredClone(formattedRestoredRecords),
62+
authContext,
63+
objectMetadataItem: objectMetadataItemWithFieldMaps,
64+
});
65+
6666
if (executionArgs.graphqlQuerySelectedFieldsResult.relations) {
6767
await this.processNestedRelationsHelper.processNestedRelations({
6868
objectMetadataMaps,

packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResol
9090
);
9191

9292
this.apiEventEmitterService.emitUpdateEvents({
93-
existingRecords: formattedExistingRecords,
94-
records: formattedUpdatedRecords,
93+
existingRecords: structuredClone(formattedExistingRecords),
94+
records: structuredClone(formattedUpdatedRecords),
9595
updatedFields: Object.keys(executionArgs.args.data),
9696
authContext,
9797
objectMetadataItem: objectMetadataItemWithFieldMaps,
@@ -101,7 +101,10 @@ export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResol
101101
await this.processNestedRelationsHelper.processNestedRelations({
102102
objectMetadataMaps,
103103
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
104-
parentObjectRecords: formattedUpdatedRecords,
104+
parentObjectRecords: [
105+
...formattedExistingRecords,
106+
...formattedUpdatedRecords,
107+
],
105108
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
106109
limit: QUERY_MAX_RECORDS,
107110
authContext,

0 commit comments

Comments
 (0)