Skip to content

Commit 929b390

Browse files
committed
fix(twenty-server): preserve input order in bulk API responses
Bulk APIs (createMany, mergeMany, findDuplicates) were returning records in arbitrary order because they use WHERE id IN (...) without ORDER BY. Fixed by reordering fetched records to match the original input order. Affected endpoints: - createMany: fetchUpsertedRecords - mergeMany: getRecordsToMerge (for dry-run and actual merge) - findDuplicates: when fetching by IDs
1 parent a07590e commit 929b390

File tree

3 files changed

+31
-7
lines changed

3 files changed

+31
-7
lines changed

packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -419,18 +419,25 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
419419
flatFieldMetadataMaps,
420420
});
421421

422+
const orderedIds = objectRecords.generatedMaps.map((record) => record.id);
423+
422424
const upsertedRecords = await queryBuilder
423425
.setFindOptions({
424426
select: columnsToSelect,
425427
})
426428
.where({
427-
id: In(objectRecords.generatedMaps.map((record) => record.id)),
429+
id: In(orderedIds),
428430
})
429431
.withDeleted()
430432
.take(QUERY_MAX_RECORDS)
431433
.getMany();
432434

433-
return upsertedRecords as ObjectRecord[];
435+
// Preserve original input order by sorting results to match orderedIds
436+
const recordsById = new Map(
437+
upsertedRecords.map((record) => [record.id, record]),
438+
);
439+
440+
return orderedIds.map((id) => recordsById.get(id) as ObjectRecord);
434441
}
435442

436443
async processQueryResult(

packages/twenty-server/src/engine/api/common/common-query-runners/common-find-duplicates-query-runner.service.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,21 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne
7171
});
7272

7373
if (isDefined(args.ids) && args.ids.length > 0) {
74-
objectRecords = (await existingRecordsQueryBuilder
74+
const fetchedRecords = (await existingRecordsQueryBuilder
7575
.where({ id: In(args.ids) })
7676
.setFindOptions({
7777
select: columnsToSelect,
7878
})
7979
.getMany()) as ObjectRecord[];
80+
81+
// Preserve original input order
82+
const recordsById = new Map(
83+
fetchedRecords.map((record) => [record.id, record]),
84+
);
85+
86+
objectRecords = args.ids
87+
.map((id) => recordsById.get(id))
88+
.filter(isDefined);
8089
} else if (args.data && !isEmpty(args.data)) {
8190
objectRecords = args.data;
8291
}

packages/twenty-server/src/engine/api/common/common-query-runners/common-merge-many-query-runner.service.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,25 +137,33 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ
137137
flatFieldMetadataMaps: context.flatFieldMetadataMaps,
138138
});
139139

140-
const recordsToMerge = await context.repository.find({
140+
const fetchedRecords = await context.repository.find({
141141
where: { id: In(args.ids) },
142142
select: columnsToSelect,
143143
});
144144

145-
if (recordsToMerge.length !== args.ids.length) {
145+
if (fetchedRecords.length !== args.ids.length) {
146146
throw new CommonQueryRunnerException(
147147
'One or more records not found',
148148
CommonQueryRunnerExceptionCode.RECORD_NOT_FOUND,
149149
{ userFriendlyMessage: msg`One or more records were not found.` },
150150
);
151151
}
152152

153+
// Preserve original input order
154+
const recordsById = new Map(
155+
fetchedRecords.map((record) => [record.id, record]),
156+
);
157+
const recordsToMerge = args.ids.map(
158+
(id) => recordsById.get(id) as ObjectRecord,
159+
);
160+
153161
if (args.dryRun && args.selectedFieldsResult.relations) {
154162
await this.processNestedRelationsHelper.processNestedRelations({
155163
flatObjectMetadataMaps: context.flatObjectMetadataMaps,
156164
flatFieldMetadataMaps: context.flatFieldMetadataMaps,
157165
parentObjectMetadataItem: context.flatObjectMetadata,
158-
parentObjectRecords: recordsToMerge as ObjectRecord[],
166+
parentObjectRecords: recordsToMerge,
159167
relations: args.selectedFieldsResult.relations as Record<
160168
string,
161169
FindOptionsRelations<ObjectLiteral>
@@ -168,7 +176,7 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ
168176
});
169177
}
170178

171-
return recordsToMerge as ObjectRecord[];
179+
return recordsToMerge;
172180
}
173181

174182
private validateAndGetPriorityRecord(

0 commit comments

Comments
 (0)