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
This commit is contained in:
+9
-1
@@ -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[];
|
||||
}
|
||||
|
||||
|
||||
+9
-1
@@ -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;
|
||||
}
|
||||
|
||||
+13
-5
@@ -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<ObjectLiteral>
|
||||
@@ -168,7 +176,7 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ
|
||||
});
|
||||
}
|
||||
|
||||
return recordsToMerge as ObjectRecord[];
|
||||
return recordsToMerge;
|
||||
}
|
||||
|
||||
private validateAndGetPriorityRecord(
|
||||
|
||||
Reference in New Issue
Block a user