fix: updated dryrun to return correct populated record (#15484)

## Description

- This PR approaches to solve
https://github.com/twentyhq/twenty/issues/15201
- updated `createDryRunResponse` to return fully populated merged record
- this way the frontend can render the populated data it as-is without
recomputing relations
- Dry-run now uses the same nested-relations population path as the
non-dryRun flow

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
This commit is contained in:
Harshit Singh
2025-11-03 18:20:14 +05:30
committed by GitHub
parent 120cec9885
commit 9e1c854b27
3 changed files with 69 additions and 21 deletions
@@ -33,7 +33,6 @@ import {
GraphqlQueryRunnerException,
GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return';
import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select';
import { hasRecordFieldValue } from 'src/engine/api/graphql/graphql-query-runner/utils/has-record-field-value.util';
@@ -73,15 +72,11 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ
recordsToMerge,
priorityRecord.id,
objectMetadataItemWithFieldMaps,
args.dryRun ?? false,
);
if (args.dryRun) {
return this.createDryRunResponse(
priorityRecord,
mergedData,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
return this.createDryRunResponse(priorityRecord, mergedData);
}
const idsToDelete = args.ids.filter((id) => id !== priorityRecord.id);
@@ -139,6 +134,9 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ
const recordsToMerge = await context.repository.find({
where: { id: In(args.ids) },
select: columnsToSelect,
...(args.dryRun
? { relations: args.selectedFieldsResult.relations }
: {}),
});
if (recordsToMerge.length !== args.ids.length) {
@@ -175,6 +173,7 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ
recordsToMerge: ObjectRecord[],
priorityRecordId: string,
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
isDryRun = false,
): Partial<ObjectRecord> {
const mergedResult: Partial<ObjectRecord> = {};
@@ -217,10 +216,18 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ
return;
}
const relationType =
isDryRun && fieldMetadata.type === FieldMetadataType.RELATION
? (fieldMetadata.settings as FieldMetadataRelationSettings)
?.relationType
: undefined;
mergedResult[fieldName] = mergeFieldValues(
fieldMetadata.type,
recordsWithValues,
priorityRecordId,
isDryRun,
relationType,
);
}
});
@@ -242,25 +249,15 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ
private createDryRunResponse(
priorityRecord: ObjectRecord,
mergedData: Partial<ObjectRecord>,
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
objectMetadataMaps: ObjectMetadataMaps,
): ObjectRecord {
const dryRunRecord = {
const dryRunRecord: ObjectRecord = {
...priorityRecord,
...mergedData,
id: uuidv4(),
deletedAt: new Date().toISOString(),
} as ObjectRecord;
};
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
return typeORMObjectRecordsParser.processRecord({
objectRecord: dryRunRecord,
objectName: objectMetadataItemWithFieldMaps.nameSingular,
take: 1,
totalCount: 1,
});
return dryRunRecord;
}
private async updatePriorityRecord(
@@ -1,4 +1,4 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { FieldMetadataType, type RelationType } from 'twenty-shared/types';
import { type EmailsMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/emails.composite-type';
import { type LinksMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/links.composite-type';
@@ -8,18 +8,32 @@ import { mergeArrayFieldValues } from './merge-array-field-values.util';
import { mergeEmailsFieldValues } from './merge-emails-field-values.util';
import { mergeLinksFieldValues } from './merge-links-field-values.util';
import { mergePhonesFieldValues } from './merge-phones-field-values.util';
import { mergeRelationFieldValuesForDryRunRecord } from './merge-relation-field-values-for-dry-run-record.util';
import { selectPriorityFieldValue } from './select-priority-field-value.util';
export const mergeFieldValues = (
fieldType: FieldMetadataType,
recordsWithValues: { value: unknown; recordId: string }[],
priorityRecordId: string,
isDryRun = false,
relationType?: RelationType,
): unknown => {
switch (fieldType) {
case FieldMetadataType.ARRAY:
case FieldMetadataType.MULTI_SELECT:
return mergeArrayFieldValues(recordsWithValues);
case FieldMetadataType.RELATION:
if (isDryRun) {
return mergeRelationFieldValuesForDryRunRecord(
recordsWithValues,
relationType,
priorityRecordId,
);
}
return selectPriorityFieldValue(recordsWithValues, priorityRecordId);
case FieldMetadataType.EMAILS:
return mergeEmailsFieldValues(
recordsWithValues as { value: EmailsMetadata; recordId: string }[],
@@ -0,0 +1,37 @@
import { type ObjectRecord, RelationType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { selectPriorityFieldValue } from 'src/engine/api/graphql/graphql-query-runner/utils/select-priority-field-value.util';
export const mergeRelationFieldValuesForDryRunRecord = (
recordsWithValues: { value: unknown; recordId: string }[],
relationType: RelationType | undefined,
priorityRecordId: string,
): ObjectRecord | ObjectRecord[] | null => {
if (relationType === RelationType.ONE_TO_MANY) {
return mergeOneToManyRelationArrays(recordsWithValues);
}
return selectPriorityFieldValue(
recordsWithValues as { value: ObjectRecord | null; recordId: string }[],
priorityRecordId,
);
};
const mergeOneToManyRelationArrays = (
recordsWithValues: { value: unknown; recordId: string }[],
): ObjectRecord[] => {
const uniqueRelationsMap = new Map<string, ObjectRecord>();
recordsWithValues.forEach(({ value }) => {
if (Array.isArray(value)) {
value.forEach((relation: ObjectRecord) => {
if (isDefined(relation?.id)) {
uniqueRelationsMap.set(relation.id, relation);
}
});
}
});
return Array.from(uniqueRelationsMap.values());
};