From 389b6ce7b258142b79a082a9d11578fefbc19d5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Sat, 24 Jan 2026 13:49:48 +0100 Subject: [PATCH] fix(twenty-server): preserve input order in createMany response (#17412) ## Summary - The `createMany` API was returning records in arbitrary order because `fetchUpsertedRecords` used `WHERE id IN (...)` without `ORDER BY` - This caused flaky tests that assumed input order was preserved (e.g., `people-merge-many.integration-spec.ts`) - Fixed by reordering the fetched records to match the original input order from `generatedMaps` ## Root Cause ```typescript // Before: No ORDER BY, so SQL returns in arbitrary order const upsertedRecords = await queryBuilder .where({ id: In(objectRecords.generatedMaps.map((record) => record.id)) }) .getMany(); // Returns in arbitrary order! ``` ## Fix ```typescript // After: Reorder results to match original input order const orderedIds = objectRecords.generatedMaps.map((record) => record.id); const upsertedRecords = await queryBuilder .where({ id: In(orderedIds) }) .getMany(); // Preserve original input order const recordsById = new Map(upsertedRecords.map((record) => [record.id, record])); return orderedIds .map((id) => recordsById.get(id)) .filter((record) => record !== undefined); ``` ## Test plan - [x] Run `people-merge-many.integration-spec.ts` multiple times - passes consistently - [x] Lint passes --- .../common-create-many-query-runner.service.ts | 10 +++++++++- ...mon-find-duplicates-query-runner.service.ts | 10 +++++++++- .../common-merge-many-query-runner.service.ts | 18 +++++++++++++----- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service.ts index ef6465da62..726ae136dc 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service.ts @@ -419,17 +419,25 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer flatFieldMetadataMaps, }); + const orderedIds = objectRecords.generatedMaps.map((record) => record.id); + const upsertedRecords = await queryBuilder .setFindOptions({ select: columnsToSelect, }) .where({ - id: In(objectRecords.generatedMaps.map((record) => record.id)), + id: In(orderedIds), }) .withDeleted() .take(QUERY_MAX_RECORDS) .getMany(); + const orderIndex = new Map(orderedIds.map((id, index) => [id, index])); + + upsertedRecords.sort( + (a, b) => (orderIndex.get(a.id) ?? 0) - (orderIndex.get(b.id) ?? 0), + ); + return upsertedRecords as ObjectRecord[]; } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-duplicates-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-duplicates-query-runner.service.ts index 217f97ecbd..79249e0035 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-duplicates-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-duplicates-query-runner.service.ts @@ -71,12 +71,20 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne }); if (isDefined(args.ids) && args.ids.length > 0) { - objectRecords = (await existingRecordsQueryBuilder + const fetchedRecords = (await existingRecordsQueryBuilder .where({ id: In(args.ids) }) .setFindOptions({ select: columnsToSelect, }) .getMany()) as ObjectRecord[]; + + const orderIndex = new Map(args.ids.map((id, index) => [id, index])); + + fetchedRecords.sort( + (a, b) => (orderIndex.get(a.id) ?? 0) - (orderIndex.get(b.id) ?? 0), + ); + + objectRecords = fetchedRecords; } else if (args.data && !isEmpty(args.data)) { objectRecords = args.data; } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-merge-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-merge-many-query-runner.service.ts index 8cea5038c7..f1628214a3 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-merge-many-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-merge-many-query-runner.service.ts @@ -137,12 +137,12 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ flatFieldMetadataMaps: context.flatFieldMetadataMaps, }); - const recordsToMerge = await context.repository.find({ + const fetchedRecords = (await context.repository.find({ where: { id: In(args.ids) }, select: columnsToSelect, - }); + })) as ObjectRecord[]; - if (recordsToMerge.length !== args.ids.length) { + if (fetchedRecords.length !== args.ids.length) { throw new CommonQueryRunnerException( 'One or more records not found', CommonQueryRunnerExceptionCode.RECORD_NOT_FOUND, @@ -150,12 +150,20 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ ); } + const orderIndex = new Map(args.ids.map((id, index) => [id, index])); + + fetchedRecords.sort( + (a, b) => (orderIndex.get(a.id) ?? 0) - (orderIndex.get(b.id) ?? 0), + ); + + const recordsToMerge = fetchedRecords; + if (args.dryRun && args.selectedFieldsResult.relations) { await this.processNestedRelationsHelper.processNestedRelations({ flatObjectMetadataMaps: context.flatObjectMetadataMaps, flatFieldMetadataMaps: context.flatFieldMetadataMaps, parentObjectMetadataItem: context.flatObjectMetadata, - parentObjectRecords: recordsToMerge as ObjectRecord[], + parentObjectRecords: recordsToMerge, relations: args.selectedFieldsResult.relations as Record< string, FindOptionsRelations @@ -168,7 +176,7 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ }); } - return recordsToMerge as ObjectRecord[]; + return recordsToMerge; } private validateAndGetPriorityRecord(