Upsert in CreateMany - fixes (#13598)
fixes https://github.com/twentyhq/twenty/issues/13553 and https://github.com/twentyhq/core-team-issues/issues/1300
This commit is contained in:
+12
-19
@@ -89,19 +89,12 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
|
||||
): Promise<InsertResult> {
|
||||
const { objectMetadataItemWithFieldMaps } = executionArgs.options;
|
||||
|
||||
const selectedColumns = buildColumnsToSelect({
|
||||
select: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
});
|
||||
|
||||
const conflictingFields = this.getConflictingFields(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
);
|
||||
const existingRecords = await this.findExistingRecords(
|
||||
executionArgs,
|
||||
conflictingFields,
|
||||
selectedColumns,
|
||||
);
|
||||
|
||||
const { recordsToUpdate, recordsToInsert } = this.categorizeRecords(
|
||||
@@ -187,7 +180,6 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
|
||||
fullPath: string;
|
||||
column: string;
|
||||
}[],
|
||||
selectedColumns: Record<string, boolean>,
|
||||
): Promise<Partial<ObjectRecord>[]> {
|
||||
const { objectMetadataItemWithFieldMaps } = executionArgs.options;
|
||||
const queryBuilder = executionArgs.repository.createQueryBuilder(
|
||||
@@ -203,12 +195,7 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
|
||||
queryBuilder.orWhere(condition);
|
||||
});
|
||||
|
||||
return await queryBuilder
|
||||
.setFindOptions({
|
||||
select: selectedColumns,
|
||||
})
|
||||
.withDeleted()
|
||||
.getMany();
|
||||
return await queryBuilder.withDeleted().getMany();
|
||||
}
|
||||
|
||||
private getValueFromPath(
|
||||
@@ -271,11 +258,17 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
|
||||
for (const field of conflictingFields) {
|
||||
const requestFieldValue = this.getValueFromPath(record, field.fullPath);
|
||||
|
||||
const existingRec = existingRecords.find(
|
||||
(existingRecord) =>
|
||||
isDefined(existingRecord[field.column]) &&
|
||||
existingRecord[field.column] === requestFieldValue,
|
||||
);
|
||||
const existingRec = existingRecords.find((existingRecord) => {
|
||||
const existingFieldValue = this.getValueFromPath(
|
||||
existingRecord,
|
||||
field.fullPath,
|
||||
);
|
||||
|
||||
return (
|
||||
isDefined(existingFieldValue) &&
|
||||
existingFieldValue === requestFieldValue
|
||||
);
|
||||
});
|
||||
|
||||
if (existingRec) {
|
||||
existingRecord = { ...record, id: existingRec.id };
|
||||
|
||||
+9
-3
@@ -5,12 +5,18 @@ import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-qu
|
||||
|
||||
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
|
||||
interface PostgreSQLError extends QueryFailedError {
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
export const handleDuplicateKeyError = (
|
||||
error: QueryFailedError,
|
||||
error: PostgreSQLError,
|
||||
context: WorkspaceQueryRunnerOptions,
|
||||
) => {
|
||||
const indexNameMatch = error.message.match(/"([^"]+)"/);
|
||||
|
||||
const duplicatedValues = error?.detail?.match(/=\(([^)]+)\)/)?.[1];
|
||||
|
||||
if (indexNameMatch) {
|
||||
const indexName = indexNameMatch[1];
|
||||
|
||||
@@ -42,9 +48,9 @@ export const handleDuplicateKeyError = (
|
||||
|
||||
if (affectedColumns?.length === 1) {
|
||||
throw new UserInputError(
|
||||
`Duplicate ${columnNames}. Please set a unique one.`,
|
||||
`Duplicate ${columnNames} ${duplicatedValues ? `with value ${duplicatedValues}` : ''}. Please set a unique one.`,
|
||||
{
|
||||
userFriendlyMessage: `This ${columnNames.toLowerCase()} is already taken. Please choose a different value.`,
|
||||
userFriendlyMessage: `This ${columnNames.toLowerCase()} ${duplicatedValues ? `with value ${duplicatedValues}` : ''} is already taken. Please choose a different value.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@ export const PERSON_GQL_FIELDS = `
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
emails {
|
||||
primaryEmail
|
||||
}
|
||||
createdAt
|
||||
deletedAt
|
||||
`;
|
||||
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
import { PERSON_GQL_FIELDS } from 'test/integration/constants/person-gql-fields.constants';
|
||||
import { createManyOperationFactory } from 'test/integration/graphql/utils/create-many-operation-factory.util';
|
||||
import { findManyOperationFactory } from 'test/integration/graphql/utils/find-many-operation-factory.util';
|
||||
import { findOneOperationFactory } from 'test/integration/graphql/utils/find-one-operation-factory.util';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
import { deleteAllRecords } from 'test/integration/utils/delete-all-records';
|
||||
|
||||
describe('people resolvers (integration)', () => {
|
||||
let person2Id: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
await deleteAllRecords('person');
|
||||
});
|
||||
|
||||
it('should create many people', async () => {
|
||||
const graphqlOperation = createManyOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
objectMetadataPluralName: 'people',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
data: [
|
||||
{
|
||||
name: {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
},
|
||||
jobTitle: 'Just Created',
|
||||
emails: {
|
||||
primaryEmail: 'john.doe@example.com',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: {
|
||||
firstName: 'Jane',
|
||||
lastName: 'Smith',
|
||||
},
|
||||
jobTitle: 'Just Created',
|
||||
emails: {
|
||||
primaryEmail: 'jane.smith@example.com',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: {
|
||||
firstName: 'Tim',
|
||||
lastName: 'Apple',
|
||||
},
|
||||
jobTitle: 'Just Created',
|
||||
emails: {
|
||||
primaryEmail: 'tim.apple@example.com',
|
||||
},
|
||||
},
|
||||
],
|
||||
upsert: true,
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequest(graphqlOperation);
|
||||
|
||||
expect(response.body.data.createPeople).toHaveLength(3);
|
||||
expect(response.body.errors).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should update many people', async () => {
|
||||
const findOneOperation = findOneOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
filter: {
|
||||
emails: {
|
||||
primaryEmail: {
|
||||
eq: 'jane.smith@example.com',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const findOneResponse = await makeGraphqlAPIRequest(findOneOperation);
|
||||
|
||||
person2Id = findOneResponse.body.data.person.id;
|
||||
|
||||
const graphqlOperation = createManyOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
objectMetadataPluralName: 'people',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
data: [
|
||||
{
|
||||
emails: {
|
||||
primaryEmail: 'john.doe@example.com',
|
||||
},
|
||||
jobTitle: 'Just Updated',
|
||||
},
|
||||
{
|
||||
id: person2Id,
|
||||
jobTitle: 'Just Updated',
|
||||
},
|
||||
],
|
||||
upsert: true,
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequest(graphqlOperation);
|
||||
|
||||
const findAllOperation = findManyOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
objectMetadataPluralName: 'people',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
});
|
||||
|
||||
const findAllResponse = await makeGraphqlAPIRequest(findAllOperation);
|
||||
|
||||
expect(findAllResponse.body.data.people.edges.length).toBe(3);
|
||||
|
||||
expect(response.body.data.createPeople).toHaveLength(2);
|
||||
expect(response.body.errors).toBeUndefined();
|
||||
|
||||
response.body.data.createPeople.forEach((person: any) => {
|
||||
expect(person.jobTitle).toEqual('Just Updated');
|
||||
});
|
||||
});
|
||||
|
||||
it('should update and create many people', async () => {
|
||||
const graphqlOperation = createManyOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
objectMetadataPluralName: 'people',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
data: [
|
||||
{
|
||||
emails: {
|
||||
primaryEmail: 'tim.apple@example.com',
|
||||
},
|
||||
jobTitle: 'Just Updated',
|
||||
},
|
||||
{
|
||||
jobTitle: 'Just Created',
|
||||
emails: {
|
||||
primaryEmail: 'paul.doe@example.com',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: person2Id,
|
||||
jobTitle: 'Email Just Updated',
|
||||
emails: {
|
||||
primaryEmail: 'jane.smith@updated.com',
|
||||
},
|
||||
},
|
||||
],
|
||||
upsert: true,
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequest(graphqlOperation);
|
||||
|
||||
const findAllOperation = findManyOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
objectMetadataPluralName: 'people',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
});
|
||||
|
||||
const findAllResponse = await makeGraphqlAPIRequest(findAllOperation);
|
||||
|
||||
expect(findAllResponse.body.data.people.edges.length).toBe(4);
|
||||
|
||||
expect(response.body.data.createPeople).toHaveLength(3);
|
||||
expect(
|
||||
response.body.data.createPeople.find(
|
||||
(person: any) => person.id === person2Id,
|
||||
).emails.primaryEmail,
|
||||
).toEqual('jane.smith@updated.com');
|
||||
expect(response.body.errors).toBeUndefined();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user