Upsert - fixes (#14358)
- add more explicit error message on upsert conflicts - increase MAX_RECORDS_QUERY constant - add soft deleted records when returning upserted records fixes https://github.com/twentyhq/private-issues/issues/300
This commit is contained in:
+2
@@ -19,4 +19,6 @@ export enum GraphqlQueryRunnerExceptionCode {
|
||||
RELATION_TARGET_OBJECT_METADATA_NOT_FOUND = 'RELATION_TARGET_OBJECT_METADATA_NOT_FOUND',
|
||||
NOT_IMPLEMENTED = 'NOT_IMPLEMENTED',
|
||||
INVALID_POST_HOOK_PAYLOAD = 'INVALID_POST_HOOK_PAYLOAD',
|
||||
UPSERT_MULTIPLE_MATCHING_RECORDS_CONFLICT = 'UPSERT_MULTIPLE_MATCHING_RECORDS_CONFLICT',
|
||||
UPSERT_MAX_RECORDS_EXCEEDED = 'UPSERT_MAX_RECORDS_EXCEEDED',
|
||||
}
|
||||
|
||||
+64
-13
@@ -1,5 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
@@ -41,6 +42,16 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
|
||||
async resolve(
|
||||
executionArgs: GraphqlQueryResolverExecutionArgs<CreateManyResolverArgs>,
|
||||
): Promise<ObjectRecord[]> {
|
||||
if (executionArgs.args.data.length > QUERY_MAX_RECORDS) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
`Maximum number of records to upsert is ${QUERY_MAX_RECORDS}.`,
|
||||
GraphqlQueryRunnerExceptionCode.UPSERT_MAX_RECORDS_EXCEEDED,
|
||||
{
|
||||
userFriendlyMessage: t`Maximum number of records to upsert is ${QUERY_MAX_RECORDS}.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const { objectMetadataItemWithFieldMaps, objectMetadataMaps } =
|
||||
executionArgs.options;
|
||||
|
||||
@@ -284,12 +295,36 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
|
||||
const recordsToInsert: Partial<ObjectRecord>[] = [];
|
||||
|
||||
for (const record of records) {
|
||||
let existingRecord: PartialObjectRecordWithId | null = null;
|
||||
const matchingRecordId = this.getMatchingRecordId(
|
||||
record,
|
||||
conflictingFields,
|
||||
existingRecords,
|
||||
);
|
||||
|
||||
for (const field of conflictingFields) {
|
||||
if (isDefined(matchingRecordId)) {
|
||||
recordsToUpdate.push({ ...record, id: matchingRecordId });
|
||||
} else {
|
||||
recordsToInsert.push(record);
|
||||
}
|
||||
}
|
||||
|
||||
return { recordsToUpdate, recordsToInsert };
|
||||
}
|
||||
|
||||
private getMatchingRecordId(
|
||||
record: Partial<ObjectRecord>,
|
||||
conflictingFields: {
|
||||
baseField: string;
|
||||
fullPath: string;
|
||||
column: string;
|
||||
}[],
|
||||
existingRecords: PartialObjectRecordWithId[],
|
||||
): string | undefined {
|
||||
const matchingRecordIds = conflictingFields.reduce<string[]>(
|
||||
(acc, field) => {
|
||||
const requestFieldValue = this.getValueFromPath(record, field.fullPath);
|
||||
|
||||
const existingRec = existingRecords.find((existingRecord) => {
|
||||
const matchingRecord = existingRecords.find((existingRecord) => {
|
||||
const existingFieldValue = this.getValueFromPath(
|
||||
existingRecord,
|
||||
field.fullPath,
|
||||
@@ -301,20 +336,35 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
|
||||
);
|
||||
});
|
||||
|
||||
if (existingRec) {
|
||||
existingRecord = { ...record, id: existingRec.id };
|
||||
break;
|
||||
if (isDefined(matchingRecord)) {
|
||||
acc.push(matchingRecord.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (existingRecord) {
|
||||
recordsToUpdate.push({ ...record, id: existingRecord.id });
|
||||
} else {
|
||||
recordsToInsert.push(record);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
if ([...new Set(matchingRecordIds)].length > 1) {
|
||||
const conflictingFieldsValues = conflictingFields
|
||||
.map((field) => {
|
||||
const value = this.getValueFromPath(record, field.fullPath);
|
||||
|
||||
return isDefined(value) ? `${field.fullPath}: ${value}` : undefined;
|
||||
})
|
||||
.filter(isDefined)
|
||||
.join(', ');
|
||||
|
||||
throw new GraphqlQueryRunnerException(
|
||||
`Multiple records found with the same unique field values for ${conflictingFieldsValues}. Cannot determine which record to update.`,
|
||||
GraphqlQueryRunnerExceptionCode.UPSERT_MULTIPLE_MATCHING_RECORDS_CONFLICT,
|
||||
{
|
||||
userFriendlyMessage: t`Multiple records found with the same unique field values for ${conflictingFieldsValues}. Cannot determine which record to update.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return { recordsToUpdate, recordsToInsert };
|
||||
return matchingRecordIds[0];
|
||||
}
|
||||
|
||||
private async processRecordsToUpdate({
|
||||
@@ -400,6 +450,7 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
|
||||
.where({
|
||||
id: In(objectRecords.generatedMaps.map((record) => record.id)),
|
||||
})
|
||||
.withDeleted()
|
||||
.take(QUERY_MAX_RECORDS)
|
||||
.getMany();
|
||||
|
||||
|
||||
+2
@@ -24,6 +24,8 @@ export const graphqlQueryRunnerExceptionHandler = (
|
||||
case GraphqlQueryRunnerExceptionCode.FIELD_NOT_FOUND:
|
||||
case GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT:
|
||||
case GraphqlQueryRunnerExceptionCode.NOT_IMPLEMENTED:
|
||||
case GraphqlQueryRunnerExceptionCode.UPSERT_MULTIPLE_MATCHING_RECORDS_CONFLICT:
|
||||
case GraphqlQueryRunnerExceptionCode.UPSERT_MAX_RECORDS_EXCEEDED:
|
||||
throw new UserInputError(error);
|
||||
case GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND:
|
||||
throw new NotFoundError(error);
|
||||
|
||||
Reference in New Issue
Block a user